Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Latest commit

 

History

History
History
147 lines (97 loc) · 11.9 KB

File metadata and controls

147 lines (97 loc) · 11.9 KB
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
[再有人问你Java内存模型是什么就把这篇文章发给他。][1]中我们曾经介绍过Java语言为了解决并发编程中存在的原子性可见性和有序性问题提供了一系列和并发处理相关的关键字比如`synchronized`、`volatile`、`final`、`concurren包`
深入理解Java虚拟机有这样一段话
> `synchronized`关键字在需要原子性可见性和有序性这三种特性的时候都可以作为其中一种解决方案看起来是万能的确大部分并发控制操作都能使用synchronized来完成
海明威在他的午后之死说过的:“冰山运动之雄伟壮观是因为他只有八分之一在水面上。”对于程序员来说,`synchronized`只是个关键字而已用起来很简单之所以我们可以在处理多线程问题时可以不用考虑太多就是因为这个关键字帮我们屏蔽了很多细节
那么本文就围绕`synchronized`展开主要介绍`synchronized`的用法、`synchronized`的原理以及`synchronized`是如何提供原子性可见性和有序性保障的等
### synchronized的用法
`synchronized`是Java提供的一个并发控制的关键字主要有两种用法分别是同步方法和同步代码块也就是说,`synchronized`既可以修饰方法也可以修饰代码块
/**
* @author Hollis 18/08/04.
*/
public class SynchronizedDemo {
//同步方法
public synchronized void doSth(){
System.out.println("Hello World");
}
//同步代码块
public void doSth1(){
synchronized (SynchronizedDemo.class){
System.out.println("Hello World");
}
}
}
`synchronized`修饰的代码块及方法在同一时间只能被单个线程访问
### synchronized的实现原理
`synchronized`,是Java中用于解决并发情况下数据同步访问的一个很重要的关键字当我们想要保证一个共享资源在同一时间只会被一个线程访问到时我们可以在代码中使用`synchronized`关键字对类或者对象加锁
[深入理解多线程)——Synchronized的实现原理][2]中我曾经介绍过其实现原理为了保证知识的完整性这里再简单介绍一下详细的内容请去原文阅读
我们对上面的代码进行反编译可以得到如下代码
public synchronized void doSth();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/hollis/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Hello World
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
通过反编译后代码可以看出对于同步方法JVM采用`ACC_SYNCHRONIZED`标记符来实现同步对于同步代码块JVM采用`monitorenter`、`monitorexit`两个指令来实现同步
[The Java® Virtual Machine Specification][3]中有关于同步方法和同步代码块的实现原理的介绍我翻译成中文如下
> 方法级的同步是隐式的同步方法的常量池中会有一个`ACC_SYNCHRONIZED`标志当某个线程要访问某个方法的时候会检查是否有`ACC_SYNCHRONIZED`,如果有设置则需要先获得监视器锁然后开始执行方法方法执行之后再释放监视器锁这时如果其他线程来请求执行方法会因为无法获得监视器锁而被阻断住值得注意的是如果在方法执行过程中发生了异常并且方法内部并没有处理该异常那么在异常被抛到方法外面之前监视器锁会被自动释放
>
> 同步代码块使用`monitorenter``monitorexit`两个指令实现可以把执行`monitorenter`指令理解为加锁执行`monitorexit`理解为释放锁每个对象维护着一个记录着被锁次数的计数器未被锁定的对象的该计数器为0当一个线程获得锁执行`monitorenter`)该计数器自增变为 1当同一个线程再次获得该对象的锁的时候计数器再次自增当同一个线程释放锁执行`monitorexit`指令的时候计数器再自减当计数器为0的时候锁将被释放其他线程便可以获得锁
无论是`ACC_SYNCHRONIZED`还是`monitorenter`、`monitorexit`都是基于Monitor实现的在Java虚拟机(HotSpot)Monitor是基于C++实现的由ObjectMonitor实现
ObjectMonitor类中提供了几个方法`enter`、`exit`、`wait`、`notify`、`notifyAll`。`sychronized`加锁的时候会调用objectMonitor的enter方法解锁的时候会调用exit方法。(关于Monitor详见[深入理解多线程)—— Moniter的实现原理][4])
### synchronized与原子性
原子性是指一个操作是不可中断的要全部执行完成要不就都不执行
我们在[Java的并发编程中的多线程问题到底是怎么回事儿?][5]中分析过线程是CPU调度的基本单位CPU有时间片的概念会根据不同的调度算法进行线程调度当一个线程获得时间片之后开始执行在时间片耗尽之后就会失去CPU使用权所以在多线程场景下由于时间片在线程间轮换就会发生原子性问题
在Java中为了保证原子性提供了两个高级的字节码指令`monitorenter``monitorexit`。前面中介绍过这两个字节码指令在Java中对应的关键字就是`synchronized`。
通过`monitorenter``monitorexit`指令可以保证被`synchronized`修饰的代码在同一时间只能被一个线程访问在锁未释放之前无法被其他线程访问到因此在Java中可以使用`synchronized`来保证方法和代码块内的操作是原子性的
> 线程1在执行`monitorenter`指令的时候会对Monitor进行加锁加锁后其他线程无法获得锁除非线程1主动解锁即使在执行过程中由于某种原因比如CPU时间片用完线程1放弃了CPU但是他并没有进行解锁而由于`synchronized`的锁是可重入的下一个时间片还是只能被他自己获取到还是会继续执行代码直到所有代码执行完这就保证了原子性
### synchronized与可见性
可见性是指当多个线程访问同一个变量时一个线程修改了这个变量的值其他线程能够立即看得到修改的值
我们在[再有人问你Java内存模型是什么就把这篇文章发给他。][1]中分析过Java内存模型规定了所有的变量都存储在主内存中每条线程还有自己的工作内存线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝线程对变量的所有操作都必须在工作内存中进行而不能直接读写主内存不同的线程之间也无法直接访问对方工作内存中的变量线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行所以就可能出现线程1改了某个变量的值但是线程2不可见的情况
前面我们介绍过`synchronized`修饰的代码在开始执行时会加锁执行完成后会进行解锁而为了保证可见性有一条规则是这样的对一个变量解锁之前必须先把此变量同步回主存中这样解锁后后续线程就可以访问到被修改后的值
所以synchronized关键字锁住的对象其值是具有可见性的
### synchronized与有序性
有序性即程序执行的顺序按照代码的先后顺序执行
我们在[再有人问你Java内存模型是什么就把这篇文章发给他。][1]中分析过除了引入了时间片以外由于处理器优化和指令重排等CPU还可能对输入代码进行乱序执行比如load->add->save 有可能被优化成load->save->add这就是可能存在有序性问题
这里需要注意的是,`synchronized`是无法禁止指令重排和处理器优化的也就是说,`synchronized`无法避免上述提到的问题
那么为什么还说`synchronized`也提供了有序性保证呢
这就要再把有序性的概念扩展一下了Java程序中天然的有序性可以总结为一句话如果在本线程内观察所有操作都是天然有序的如果在一个线程中观察另一个线程所有操作都是无序的
以上这句话也是深入理解Java虚拟机中的原句但是怎么理解呢周志明并没有详细的解释这里我简单扩展一下这其实和`as-if-serial语义`有关
`as-if-serial`语义的意思指不管怎么重排序编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变编译器和处理器无论如何优化都必须遵守`as-if-serial`语义
这里不对`as-if-serial语义`详细展开了简单说就是,`as-if-serial语义`保证了单线程中指令重排是有一定的限制的而只要编译器和处理器都遵守了这个语义那么就可以认为单线程程序是按照顺序执行的当然实际上还是有重排的只不过我们无须关心这种重排的干扰
所以呢由于`synchronized`修饰的代码同一时间只能被同一线程访问那么也就是单线程执行的所以可以保证其有序性
### synchronized与锁优化
前面介绍了`synchronized`的用法原理以及对并发编程的作用是一个很好用的关键字
`synchronized`其实是借助Monitor实现的在加锁时会调用objectMonitor的`enter`方法解锁的时候会调用`exit`方法事实上只有在JDK1.6之前synchronized的实现才会直接调用ObjectMonitor的`enter``exit`,这种锁被称之为重量级锁
所以在JDK1.6中出现对锁进行了很多的优化进而出现轻量级锁偏向锁锁消除适应性自旋锁锁粗化(自旋锁在1.4就有只不过默认的是关闭的jdk1.6是默认开启的),这些操作都是为了在线程之间更高效的共享数据解决竞争问题
关于自旋锁锁粗化和锁消除可以参考[深入理解多线程)—— Java虚拟机的锁优化技术][6],关于轻量级锁和偏向锁已经在排期规划中我后面会有文章单独介绍将独家发布在我的博客(http://www.hollischuang.com)和公众号(Hollis)中,敬请期待。
好啦关于`synchronized`关键字我们介绍了其用法原理以及如何保证的原子性顺序性和可见性同时也扩展的留下了锁优化相关的资料及思考后面我们会继续介绍`volatile`关键字以及他和`synchronized`的区别等敬请期待
[1]: http://www.hollischuang.com/archives/2550
[2]: http://www.hollischuang.com/archives/1883
[3]: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10
[4]: http://www.hollischuang.com/archives/2030
[5]: http://www.hollischuang.com/archives/2618
[6]: http://www.hollischuang.com/archives/2344
Morty Proxy This is a proxified and sanitized view of the page, visit original site.