深入理解java | synchronized关键字及其原理解读

大部分人工作几年后,对java基础的理解仿佛还白头如新。一方面码农的工作都很忙,另一方面各种框架层出不穷占据了我们很多的精力,导致我们对java基础的忽视,很多人可能很久没都看过jdk源码了。这次准备写一个深入理解java系列,和大家一起回顾java基础,深入理解java的各个小知识点。

 

01

synchronized产生背景

在并发编程中,共享数据(也叫临界资源)的并发读写极可能引起线程安全问题。如,数据库、代码中的静态变量、文件资源等都是共享的资源,对他们的并发操作是导致线程安全的主要原因。

 

1.  看一段普通并发操作的代码

1/**
 2 * @author 无刀流
 3 */
 4public class ConcurrentIncrease {
 5
 6    // 共享变量(共享资源)
 7    public static int count = 0;
 8
 9    // 普通方法
10    public static void countIncrease() {
11        count++;
12    }
13
14    public static void main(String[] args) {
15
16        for (int i=0; i<1000; i++) {
17            new Thread( new Runnable() {
18                @Override
19                    public void run() {
20
21                        // 所有子线程休息 10s 后执行,增加并发执行的概率
22                        try {
23                            TimeUnit.SECONDS.sleep(10);
24                        } catch (InterruptedException e) {
25                            e.printStackTrace();
26                        }
27
28                        countIncrease();
29                    }
30                }
31            ).start();
32        }
33
34        // 主线程休息 30s 保证所有自线程执行完
35        try {
36            TimeUnit.SECONDS.sleep(30);
37        } catch (InterruptedException e) {
38            e.printStackTrace();
39        }
40
41        System.out.println("执行1000次并发自增操作, count = " + count);
42    }
43}

 

上述代码的目的是并发对 count 变量执行加1操作,实现上同时启动 1000 个线程,并同时休眠 10s 钟后执行,增加线程并发执行的几率。期望的执行结果是累加1000次,count 值变为1000。执行多次后,实际的执行结果并不是我们我们所期望的,获得的结果总是比1000小,其中一次执行结果,如下。

 

深入理解java | synchronized关键字及其原理解读_第1张图片

 

可见,在多线程并发环境下,原本单线程操作不存在的问题会大量产生。需要我们对多线程环境下并发执行理解的更加深入。

 

2. 多线程安全问题原因分析

我们来分析下产生问题的原因,现代计算机系统都是多级存储结构,当多个线程并发执行时,共享数据都先从主存加载到线程工作内存,线程执行时都是用工作内存的数据完成计算赋值等操作,而此时主存里共享数据可能已经被改掉,这样再把执行的结果回写主存时就会覆盖线程执行期间的操作结果,非我们预期的结果。如图:

 

深入理解java | synchronized关键字及其原理解读_第2张图片

举个栗子:我把自己冷冻起来,几百年后,一队考古机器人找到了我,把我叫醒了,我如果还按照之前记忆去行事,比如说汉语去尝试交流,那完蛋了,几百年后可能大家都是用脑芯片交流,瞬间完成 几T 的数据交流。也就是我(线程内部)与外面的世界(主存)脱离了,外届的变化我不知道,我的存在外届也不知道。多线程并发安全问题就是这个原因,并发执行时,每个线程内部的数据和主存的数据和其他线程内部的数据,如何同步成了问题

                                                     ——无刀流

3. synchronized如何解决并发问题----主要功能介绍

synchronized主要功能是把并行执行,变成串行执行来解决并发问题。被synchronized修饰的代码块,多线程执行时,每个线程都要先竞争获取相应锁(执行权),获取到锁的线程才有资格去执行代码块,这样共享资源同一时刻只能有一个线程在操作,避免了并发,以此来解决线程安全问题,如下图。

 

深入理解java | synchronized关键字及其原理解读_第3张图片

 

4. synchronized三大主要用途

1).  在多线程执行环境下,synchronized关键字可以保证同一时刻只有一个线程可以执行某个方法或者某段代码块(方法和代码块中的共享资源就不会被并发操作)

2).  synchronized同时可以保证共享数据的可见性(可以替代volatile,synchronized现在性能已经很好了)

3).  被synchronized修饰的代码段,编译器不会对其重排序,最强的内存屏障,也是保证数据可见性(第2点)的原因

 

02

synchronized的四种使用方法

 

1. 修饰静态方法,实质是在进入方法前获取类对象的内置锁,在退出方法时释放锁,来完成控制并发

2. 通过类对象,修饰代码块,实质也是在进入方法块前,获得修饰指定类对象的内置锁,在退出方法块时,释放锁,来完成并发控制

3. 修饰普通方法,实质是在进入方法前获取当前对象实例的内置锁,在方法结束时再释放锁

4. 通过普通对象,修饰代码块,实质也是在进入方法块前,获得修饰指定对象的内置锁,在退出方法块时,释放锁,来完成并发控制

 

1).  修饰静态方法,实现 1000 次并发加操作

1/**
 2 * @author 无刀流
 3 */
 4public class ConcurrentAtomicIncrease {
 5
 6    // 共享变量
 7    public static int count = 0;
 8
 9    /**
10     * synchronized 用法一: 修饰静态方法
11     * 本质是 synchronized(ConcurrentAtomicIncrease.class), 通过当前类对象锁来实现控制并发
12     */
13    public synchronized static void countAtomicIncrease() {
14
15        // method before 进入方法前获取 ConcurrentAtomicIncrease.class的锁
16        // 相当于执行 ConcurrentAtomicIncrease.class.lock()
17
18        count++;
19
20        // method after 执行完方法内容后,释放 ConcurrentAtomicIncrease.class的锁
21        // 相当于执行 ConcurrentAtomicIncrease.class.unlock()
22
23    }
24
25    public static void main(String[] args) {
26
27        for (int i=0; i<1000; i++) {
28            new Thread(new Runnable() {
29                @Override
30                    public void run() {
31
32                        // 所有子线程休息 10s 后执行,增加并发执行的概率
33                        try {
34                            TimeUnit.SECONDS.sleep(10);
35                        } catch (InterruptedException e) {
36                            e.printStackTrace();
37                        }
38
39                        countAtomicIncrease();
40                    }
41                }
42            ).start();
43        }
44
45        // 主线程休息 30s 保证所有自线程执行完
46        try {
47            TimeUnit.SECONDS.sleep(30);
48        } catch (InterruptedException e) {
49            e.printStackTrace();
50        }
51
52        System.out.println("执行1000次并发自增操作, count = " + count);
53    }
54}

 

2). 通过类对象,修饰代码块,实现 1000 次并发加操作

 1/**
 2 * @author 无刀流
 3 */
 4public class ConcurrentStaticBlock {
 5
 6    public static int count = 0;
 7
 8    public static void main(String[] args) {
 9
10        for (int i=0; i<1000; i++) {
11            new Thread(new Runnable() {
12                @Override
13                public void run() {
14
15                    /**
16                     * synchronized 用法二: 修饰静态方法
17                     * 本质是 synchronized(ConcurrentAtomicIncrease.class), 通过当前类对象锁来实现控制并发
18                     */
19                    synchronized (ConcurrentStaticBlock.class) {
20                        // 先获取 ConcurrentStaticBlock.class 类对象内置锁
21                        // 相当于执行 ConcurrentStaticBlock.class.lock()
22                        count++;
23                        // 代码块执行完,释放锁,相当于执行 ConcurrentStaticBlock.class.unlock()
24                    }
25                }
26            }).start();
27        }
28
29        try {
30            TimeUnit.SECONDS.sleep(20);
31        } catch (InterruptedException e) {
32            e.printStackTrace();
33        }
34
35        System.out.println("count = " + count);
36    }
37}

 

 

 

 

3).  修饰普通方法,实现 1000 次并发加操作

 1/**
 2 * @author 无刀流
 3 */
 4public class ConcurrentPlainAtomicIncrease {
 5
 6    // 共享变量
 7    public static int count = 0;
 8
 9    // synchronized 用法三:关键字修饰的普通方法
10    public synchronized void countAtomicIncrease() {
11        // method before 获取this当前实例对象的锁,相当于执行 this.lock();
12        count++;
13        // method after 执行完释放锁,相当于执行 this.unlock();
14    }
15
16    public static void main(String[] args) {
17
18        ConcurrentPlainAtomicIncrease lock = new ConcurrentPlainAtomicIncrease();
19
20        for (int i=0; i<1000; i++) {
21            new Thread(new Runnable() {
22                @Override
23                    public void run() {
24
25                        // 所有子线程休息 10s 后执行,增加并发执行的概率
26                        try {
27                            TimeUnit.SECONDS.sleep(10);
28                        } catch (InterruptedException e) {
29                            e.printStackTrace();
30                        }
31
32                        // 注意下面这种方式不能实现控制并发访问,每次都是不同的实例,获取的锁也是不同的锁
33                        // new ConcurrentPlainAtomicIncrease().countAtomicIncrease();
34
35                        lock.countAtomicIncrease();
36                    }
37                }
38            ).start();
39        }
40
41        // 主线程休息 30s 保证所有自线程执行完
42        try {
43            TimeUnit.SECONDS.sleep(30);
44        } catch (InterruptedException e) {
45            e.printStackTrace();
46        }
47
48        System.out.println("执行1000次并发自增操作, count = " + count);
49    }
50}

 

 

 

 

4). 通过实例对象,修饰代码块,实现 1000 次并发加操作

 1/**
 2 * @author 无刀流
 3 */
 4public class ConcurrentStaticBlockAtomicIncrease {
 5
 6    // 共享变量
 7    public static int count = 0;
 8
 9    public static void main(String[] args) {
10
11        Object lock = new Object();
12        for (int i=0; i<1000; i++) {
13            new Thread(new Runnable() {
14                @Override
15                    public void run() {
16
17                        // 所有子线程休息 10s 后执行,增加并发执行的概率
18                        try {
19                            TimeUnit.SECONDS.sleep(10);
20                        } catch (InterruptedException e) {
21                            e.printStackTrace();
22                        }
23
24                        // synchronized 用法四:获取实例对象锁来实现
25                        synchronized (lock) {
26                            // 获取lock对象锁, 相当于执行lock.lock
27                            count++;
28                            // 代码块执行完, 释放对象锁, 相当于执行lock.unlock
29                        }
30                    }
31                }
32            ).start();
33        }
34
35        // 主线程休息 30s 保证所有自线程执行完
36        try {
37            TimeUnit.SECONDS.sleep(30);
38        } catch (InterruptedException e) {
39            e.printStackTrace();
40        }
41
42        System.out.println("执行1000次并发自增操作, count = " + count);
43    }
44}

 

上述四种synchronized用法都可以解决并发问题,四种方式并发执行1000次叠加操作,结果都和预期一样,都是count=1000,选择其中一种执行结果,如下

深入理解java | synchronized关键字及其原理解读_第4张图片

 

总结:

1. 修饰静态方法

优点:使用方便,相当于类衍生的所有实例

2. 通过类对象,修饰静态代码块

注意点:和1差不多,控制粒度可以适当更精细些

3. 修饰普通方法

注意点:一定是同一个对象实例调用才可以。 obj1.aaa();   obj2.aaa(); 这样调用其实没有刁作用哈

4. 通过实例对象,修饰普通方法

优点:可以精细控制并发代码块

注意点:和3一样,一定要作用同一个对象才可以。  synchronized(obj1) {...}; synchronized(obj2){...}; 这样其实也没有用,不是同一个对象下

 

03

synchronized的实现原理

1. 通过 javap -v 反汇编看下 synchronized代码段

源代码:

1/**
 2 * @author 无刀流
 3 */
 4public class Test {
 5    public static void main(String[] args) {
 6
 7        Test test = new Test();
 8
 9        // 同步 代码块
10        synchronized (test) {
11            System.out.println("==========");
12        }
13
14        // 同步 方法
15        test.testSynchronized();
16    }
17
18    public static synchronized void testSynchronized() {
19        System.out.println("-----------");
20    }
21}

 

javap -v 后

 1192:lock sanfeng$ javap -c Test.class 
  2Compiled from "Test.java"
  3public class lock.Test {
  4  public lock.Test();
  5    Code:
  6       0: aload_0
  7       1: invokespecial #1                  // Method java/lang/Object."":()V
  8       4: return
  9192:lock sanfeng$ javap -v Test.class 
 10Classfile /Users/sanfeng/项目/idea/sanfeng/production/production/sanfeng/lock/Test.class
 11  Last modified 2018-8-10; size 782 bytes
 12  MD5 checksum b6e0f39200751ce0fe354906329b695c
 13  Compiled from "Test.java"
 14public class lock.Test
 15  minor version: 0
 16  major version: 52
 17  flags: ACC_PUBLIC, ACC_SUPER
 18Constant pool:
 19   #1 = Methodref          #9.#30         // java/lang/Object."":()V
 20   #2 = Class              #31            // lock/Test
 21   #3 = Methodref          #2.#30         // lock/Test."":()V
 22   #4 = Fieldref           #32.#33        // java/lang/System.out:Ljava/io/PrintStream;
 23   #5 = String             #34            // ==========
 24   #6 = Methodref          #35.#36        // java/io/PrintStream.println:(Ljava/lang/String;)V
 25   #7 = Methodref          #2.#37         // lock/Test.testSynchronized:()V
 26   #8 = String             #38            // -----------
 27   #9 = Class              #39            // java/lang/Object
 28  #10 = Utf8               
 29  #11 = Utf8               ()V
 30  #12 = Utf8               Code
 31  #13 = Utf8               LineNumberTable
 32  #14 = Utf8               LocalVariableTable
 33  #15 = Utf8               this
 34  #16 = Utf8               Llock/Test;
 35  #17 = Utf8               main
 36  #18 = Utf8               ([Ljava/lang/String;)V
 37  #19 = Utf8               args
 38  #20 = Utf8               [Ljava/lang/String;
 39  #21 = Utf8               test
 40  #22 = Utf8               StackMapTable
 41  #23 = Class              #20            // "[Ljava/lang/String;"
 42  #24 = Class              #31            // lock/Test
 43  #25 = Class              #39            // java/lang/Object
 44  #26 = Class              #40            // java/lang/Throwable
 45  #27 = Utf8               testSynchronized
 46  #28 = Utf8               SourceFile
 47  #29 = Utf8               Test.java
 48  #30 = NameAndType        #10:#11        // "":()V
 49  #31 = Utf8               lock/Test
 50  #32 = Class              #41            // java/lang/System
 51  #33 = NameAndType        #42:#43        // out:Ljava/io/PrintStream;
 52  #34 = Utf8               ==========
 53  #35 = Class              #44            // java/io/PrintStream
 54  #36 = NameAndType        #45:#46        // println:(Ljava/lang/String;)V
 55  #37 = NameAndType        #27:#11        // testSynchronized:()V
 56  #38 = Utf8               -----------
 57  #39 = Utf8               java/lang/Object
 58  #40 = Utf8               java/lang/Throwable
 59  #41 = Utf8               java/lang/System
 60  #42 = Utf8               out
 61  #43 = Utf8               Ljava/io/PrintStream;
 62  #44 = Utf8               java/io/PrintStream
 63  #45 = Utf8               println
 64  #46 = Utf8               (Ljava/lang/String;)V
 65{
 66  public lock.Test();
 67    descriptor: ()V
 68    flags: ACC_PUBLIC
 69    Code:
 70      stack=1, locals=1, args_size=1
 71         0: aload_0
 72         1: invokespecial #1                  // Method java/lang/Object."":()V
 73         4: return
 74      LineNumberTable:
 75        line 6: 0
 76      LocalVariableTable:
 77        Start  Length  Slot  Name   Signature
 78            0       5     0  this   Llock/Test;
 79
 80  public static void main(java.lang.String[]);
 81    descriptor: ([Ljava/lang/String;)V
 82    flags: ACC_PUBLIC, ACC_STATIC
 83    Code:
 84      stack=2, locals=4, args_size=1
 85         0: new           #2                  // class lock/Test
 86         3: dup
 87         4: invokespecial #3                  // Method "":()V
 88         7: astore_1
 89         8: aload_1
 90         9: dup
 91        10: astore_2
 92        11: monitorenter
 93        12: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
 94        15: ldc           #5                  // String ==========
 95        17: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
 96        20: aload_2
 97        21: monitorexit
 98        22: goto          30
 99        25: astore_3
100        26: aload_2
101        27: monitorexit
102        28: aload_3
103        29: athrow
104        30: aload_1
105        31: pop
106        32: invokestatic  #7                  // Method testSynchronized:()V
107        35: return
108      Exception table:
109         from    to  target type
110            12    22    25   any
111            25    28    25   any
112      LineNumberTable:
113        line 9: 0
114        line 12: 8
115        line 13: 12
116        line 14: 20
117        line 17: 30
118        line 18: 35
119      LocalVariableTable:
120        Start  Length  Slot  Name   Signature
121            0      36     0  args   [Ljava/lang/String;
122            8      28     1  test   Llock/Test;
123      StackMapTable: number_of_entries = 2
124        frame_type = 255 /* full_frame */
125          offset_delta = 25
126          locals = [ class "[Ljava/lang/String;", class lock/Test, class java/lang/Object ]
127          stack = [ class java/lang/Throwable ]
128        frame_type = 250 /* chop */
129          offset_delta = 4
130
131  public static synchronized void testSynchronized();
132    descriptor: ()V
133    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
134    Code:
135      stack=2, locals=0, args_size=0
136         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
137         3: ldc           #8                  // String -----------
138         5: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
139         8: return
140      LineNumberTable:
141        line 21: 0
142        line 22: 8
143}
144SourceFile: "Test.java"
145=

 

我们注意下高亮的代码  monitorenter, monitorexit, ACC_SYNCHRONIZED

查看oracle官方文档:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10

 

 

2.11.10. Synchronization

The Java Virtual Machine supports synchronization of both methods and sequences of instructions within a method by a single synchronization construct: the monitor.

Method-level synchronization is performed implicitly, as part of method invocation and return (§2.11.8).A synchronized method is distinguished in the run-time constant pool's method_info structure (§4.6) by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor,invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.

Synchronization of sequences of instructions is typically used to encode the synchronized block of the Java programming language. The Java Virtual Machine supplies the monitorenter and monitorexit instructions to support such language constructs. Proper implementation of synchronized blocks requires cooperation from a compiler targeting the Java Virtual Machine (§3.14).

Structured locking is the situation when, during a method invocation, every exit on a given monitor matches a preceding entry on that monitor. Since there is no assurance that all code submitted to the Java Virtual Machine will perform structured locking, implementations of the Java Virtual Machine are permitted but not required to enforce both of the following two rules guaranteeing structured locking. Let T be a thread and M be a monitor. Then:

 The number of monitor entries performed by T on M during a method invocation must equal the number of monitor exits performed by T on M during the method invocation whether the method invocation completes normally or abruptly. 

 At no point during a method invocation may the number of monitor exits performed by T on M since the method invocation exceed the number of monitor entries performed by T on M since the method invocation.

Note that the monitor entry and exit automatically performed by the Java Virtual Machine when invoking a synchronized method are considered to occur during the calling method's invocation.

 

这段话的意思是,synchronized的底层是对应一个monitor对象,通过进入这个对象和退出这个对象来实现同步操作。对于synchronized修饰的方法,是隐世调用的monitor对象。如下:When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor,

 

我们再看下 monitorenter官方文档的介绍:

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html

深入理解java | synchronized关键字及其原理解读_第5张图片

 

这个文档的意思如下:

1). 每个对象都有一个对应的监视器monitor

2). monitor的进入数为0,则线程可以进入monitor,进入后,进入数置为1,该线程成为monitor的所有者。

3). 如果线程已经获取monitor,任何时候都可以重新进入,再次进入后monitor的进入数再加1. 这就是说明monitor支持重入,也就是synchronized也是支持重入的。

4). 如果monitor被其他线程占用,当前线程想要进入monitor,则需要先进入阻塞状态,直到monitor的进入数为0,可以重新尝试获取monitor的所有权。

 

synchronized关键字,就是通过monitor这种只能有一个线程获得进入机会,来实现锁机制的

 

04

synchronized总结

 

 

1. synchronized 可以解决线程并发问题

2. synchronized 有四种使用方法

3. 每个java对象都关联一个监视器monitor

4. synchronized 实现原理是通过进入退出monitor来实现的

5. synchronized 是支持重入的锁,因为获取monitor的线程可以持续进入

6. synchronized 性能不低,从偏向锁->轻量锁->重量锁(大家可以查下官方文档)

7. monitor 很强大,wait、notify、notifyAll、synchronized 都是monitor衍生功能(monitor通过内部的两个队列实现,大家也可以查下,这篇不介绍了)

你可能感兴趣的:(java,synchronized)