大部分人工作几年后,对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小,其中一次执行结果,如下。
可见,在多线程并发环境下,原本单线程操作不存在的问题会大量产生。需要我们对多线程环境下并发执行理解的更加深入。
2. 多线程安全问题原因分析
我们来分析下产生问题的原因,现代计算机系统都是多级存储结构,当多个线程并发执行时,共享数据都先从主存加载到线程工作内存,线程执行时都是用工作内存的数据完成计算赋值等操作,而此时主存里共享数据可能已经被改掉,这样再把执行的结果回写主存时就会覆盖线程执行期间的操作结果,非我们预期的结果。如图:
举个栗子:我把自己冷冻起来,几百年后,一队考古机器人找到了我,把我叫醒了,我如果还按照之前记忆去行事,比如说汉语去尝试交流,那完蛋了,几百年后可能大家都是用脑芯片交流,瞬间完成 几T 的数据交流。也就是我(线程内部)与外面的世界(主存)脱离了,外届的变化我不知道,我的存在外届也不知道。多线程并发安全问题就是这个原因,并发执行时,每个线程内部的数据和主存的数据和其他线程内部的数据,如何同步成了问题
——无刀流
3. synchronized如何解决并发问题----主要功能介绍
synchronized主要功能是把并行执行,变成串行执行来解决并发问题。被synchronized修饰的代码块,多线程执行时,每个线程都要先竞争获取相应锁(执行权),获取到锁的线程才有资格去执行代码块,这样共享资源同一时刻只能有一个线程在操作,避免了并发,以此来解决线程安全问题,如下图。
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,选择其中一种执行结果,如下
总结:
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
这个文档的意思如下:
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通过内部的两个队列实现,大家也可以查下,这篇不介绍了)