32
32
在java代码中使用synchronized可是使用在代码块和方法中,根据Synchronized用的位置可以有这些使用场景:
33
33
34
34
35
- ![ Synchronized的使用场景] ( https://user-gold-cdn.xitu.io/2018/4/30/16315cc79aaac173?w=700&h=413&f= png&s=33838 )
35
+ ![ Synchronized的使用场景] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/synchronized%E7%9A%84%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF. png )
36
36
37
37
38
38
如图,synchronized可以用在** 方法** 上也可以使用在** 代码块** 中,其中方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。而使用在代码块中也可以分为三种,具体的可以看上面的表格。这里的需要注意的是:** 如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系** 。
55
55
56
56
57
57
58
- ![ SynchronizedDemo.class] ( https://user-gold-cdn.xitu.io/2018/4/30/16315cce259af0d2?w=700&h=330&f= png&s=68919 )
58
+ ![ SynchronizedDemo.class] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/synchronizedDemo.class. png )
59
59
60
60
61
61
如图,上面用黄色高亮的部分就是需要注意的部分了,这也是添Synchronized关键字之后独有的。执行同步代码块后首先要先执行**monitorenter**指令,退出的时候**monitorexit**指令。通过分析之后可以看出,使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是**互斥**的,即同一时刻只有一个线程能够获取到monitor。上面的demo中在执行完同步代码块之后紧接着再会去执行一个静态同步方法,而这个方法锁的对象依然就这个类对象,那么这个正在执行的线程还需要获取该锁吗?答案是不必的,从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是**锁的重入性**,即在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。**每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一**。
64
64
65
65
下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:
66
66
67
- ![ 对象,对象监视器,同步队列和线程状态的关系] ( https://user-gold-cdn.xitu.io/2018/4/30/16315cd5fa7cf91c?w=700&h=261&f= png&s=54962 )
67
+ ![ 对象,对象监视器,同步队列和线程状态的关系] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/%E5%AF%B9%E8%B1%A1%EF%BC%8C%E5%AF%B9%E8%B1%A1%E7%9B%91%E8%A7%86%E5%99%A8%EF%BC%8C%E5%90%8C%E6%AD%A5%E9%98%9F%E5%88%97%E5%92%8C%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E7%9A%84%E5%85%B3%E7%B3%BB. png )
68
68
69
69
该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
70
70
88
88
89
89
90
90
91
- ![ synchronized的happens-before关系] ( https://user-gold-cdn.xitu.io/2018/4/30/16315ce6ea84f240?w=650&h=629&f= png&s=61572 )
91
+ ![ synchronized的happens-before关系] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/synchronized%E7%9A%84happens-before%E5%85%B3%E7%B3%BB. png )
92
92
93
93
94
94
在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:** 线程A释放锁happens-before线程B加锁** ,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。现在我们来重点关注2 happens-before 5,通过这个关系我们可以得出什么?
100
100
101
101
废话不多说依旧先上图。
102
102
103
- ![ 线程A写共享变量] ( https://user-gold-cdn.xitu.io/2018/4/30/16315cef21fd3ad8?w=557&h=440&f= png&s=10816 )
103
+ ![ 线程A写共享变量] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/%E7%BA%BF%E7%A8%8BA%E5%86%99%E5%85%B1%E4%BA%AB%E5%8F%98%E9%87%8F. png )
104
104
105
105
106
106
从上图可以看出,线程A会首先先从主内存中读取共享变量a=0的值然后将该变量拷贝到自己的本地内存,进行加一操作后,再将该值刷新到主内存,整个过程即为线程A 加锁-->执行临界区代码-->释放锁相对应的内存语义。
107
107
108
108
109
109
110
- ![ 线程B读共享变量] ( https://user-gold-cdn.xitu.io/2018/4/30/16315cf41661491e?w=564&h=458&f= png&s=14468 )
110
+ ![ 线程B读共享变量] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/%E7%BA%BF%E7%A8%8BB%E8%AF%BB%E5%85%B1%E4%BA%AB%E5%8F%98%E9%87%8F. png )
111
111
112
112
线程B获取锁的时候同样会从主内存中共享变量a的值,这个时候就是最新的值1,然后将该值拷贝到线程B的工作内存中去,释放锁的时候同样会重写到主内存中。
113
113
@@ -153,13 +153,13 @@ CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使
153
153
154
154
在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁怎么理解?无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。32为JVM Mark Word默认存储结构为(注: java 对象头以及下面的锁状态变化摘自《java并发编程的艺术》一书,该书我认为写的足够好,就没在自己组织语言班门弄斧了):
155
155
156
- ![ Mark Word存储结构] ( https://user-gold-cdn.xitu.io/2018/4/30/16315cff10307a29?w=700&h=71&f= png&s=23717 )
156
+ ![ Mark Word存储结构] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/Mark%20Word%E5%AD%98%E5%82%A8%E7%BB%93%E6%9E%84. png )
157
157
158
158
如图在Mark Word会默认存放hasdcode,年龄值以及锁标志位等信息。
159
159
160
160
Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:** 无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态** ,这几个状态会随着竞争情况逐渐升级。** 锁可以升级但不能降级** ,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。对象的MarkWord变化为下图:
161
161
162
- ![ Mark Word状态变化] ( https://user-gold-cdn.xitu.io/2018/4/30/16315d056598e4c2?w=700&h=151&f= png&s=47968 )
162
+ ![ Mark Word状态变化] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/Mark%20Word%E7%8A%B6%E6%80%81%E5%8F%98%E5%8C%96. png )
163
163
164
164
165
165
@@ -177,14 +177,14 @@ HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多
177
177
偏向锁使用了一种** 等到竞争出现才释放锁** 的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
178
178
179
179
180
- ![ 偏向锁撤销流程] ( https://user-gold-cdn.xitu.io/2018/4/30/16315d0b13b37da4?w=567&h=736&f= png&s=72325 )
180
+ ![ 偏向锁撤销流程] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/%E5%81%8F%E5%90%91%E9%94%81%E6%92%A4%E9%94%80%E6%B5%81%E7%A8%8B. png )
181
181
182
182
183
183
如图,偏向锁的撤销,需要等待** 全局安全点** (在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word** 要么** 重新偏向于其他线程,** 要么** 恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
184
184
185
185
下图线程1展示了偏向锁获取的过程,线程2展示了偏向锁撤销的过程。
186
186
187
- ![ 偏向锁获取和撤销流程] ( https://user-gold-cdn.xitu.io/2018/4/30/16315cb9175365f5?w=630&h=703&f= png&s=160223 )
187
+ ![ 偏向锁获取和撤销流程] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/%E5%81%8F%E5%90%91%E9%94%81%E8%8E%B7%E5%8F%96%E5%92%8C%E6%92%A4%E9%94%80%E6%B5%81%E7%A8%8B. png )
188
188
189
189
> ** 如何关闭偏向锁**
190
190
@@ -201,12 +201,12 @@ HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多
201
201
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。
202
202
203
203
204
- ![ 轻量级锁加锁解锁以及锁膨胀] ( https://user-gold-cdn.xitu.io/2018/4/30/16315cb9193719c2?w=794&h=772&f= png&s=287958 )
204
+ ![ 轻量级锁加锁解锁以及锁膨胀] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/%E8%BD%BB%E9%87%8F%E7%BA%A7%E9%94%81%E5%8A%A0%E9%94%81%E8%A7%A3%E9%94%81%E4%BB%A5%E5%8F%8A%E9%94%81%E8%86%A8%E8%83%80. png )
205
205
206
206
因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
207
207
## 3.5 各种锁的比较 ##
208
208
209
- ![ 各种锁的对比] ( https://user-gold-cdn.xitu.io/2018/4/30/16315cb91da523d9?w=800&h=193&f= png&s=116058 )
209
+ ![ 各种锁的对比] ( https://github.com/fancycoderzf/Java-concurrency/blob/master/04.%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3synchronized/%E5%90%84%E7%A7%8D%E9%94%81%E7%9A%84%E5%AF%B9%E6%AF%94. png )
210
210
211
211
# 4. 一个例子 #
212
212
经过上面的理解,我们现在应该知道了该怎样解决了。更正后的代码为:
0 commit comments