深入理解 synchronized
一、引言
对于 Java
开发者而言,关于 并发编程,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。
但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。
本期 并发编程 解析系列文章,将带你领略 并发编程 的奥秘
废话不多说,发车!
二、synchronized基本使用
synchronized
的使用一般就是 同步方法 和 同步代码块。
1、同步方法
1.1 静态方法
public synchronized static void show() {
System.out.println("我是show方法");
}
1.2 非静态方法
public synchronized void showStatic() {
System.out.println("我是showStatic方法");
}
1.3 区别
静态方法:锁的是当前的类
非静态方法:锁的是当前的对象
我们来个例子模拟一下:
public class SynchronizedTest {
public static void main(String[] args) {
// 直接执行MyTest类静态方法【这里锁的是MyTest类】
MyTest.showStatic();
MyTest myTest = new MyTest();
// 创建对象,执行其show方法【这里锁的是myTest这个对象】
myTest.show();
}
}
class MyTest {
public synchronized void show() {
System.out.println("我是show方法");
}
public synchronized static void showStatic() {
System.out.println("我是showStatic方法");
}
}
2、代码块
对于代码块来说,我们经常有下面两种写法:
synchronized (myTest){
// 执行业务逻辑
}
synchronized (MyTest.class){
// 执行业务逻辑
}
这种的写法也是上面我们说的,分别锁的是 对象 和 类
这块的基本使用就讲到这里,我们继续往后看
三、synchronized优化
1、背景
这里大家有没有一个疑惑,这个 synchronized
关键词为啥要优化?不优化不行嘛?
在 JDK1.5
的时候,Doug Lee
推出了 ReentrantLock
,ReentrantLock
的性能远高于 synchronized
,所以 JDK
团队就在 JDK1.6
中,对 synchronized
做了大量的优化。
简单来说,你 JDK
团队再不去优化,都去用 Doug Lee
定义的 ReentrantLock
,JDK
团队的脸还往哪里放
到这里,你是不是感觉 Doug Lee
这个哥们特别牛逼,我们看看这哥们的背景:gee.cs.oswego.edu/
为 Java 贡献了 HashMap
和 java.util.concurrent
,只能说:牛逼
2、优化维度
2.1 锁消除
在 synchronized
修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,你即便写了 synchronized
,他也不会触发。
如下:
public synchronized void method(){
// 没有操作临界资源
// 此时这个方法的synchronized你可以认为木有~~
}
临界资源:一次仅允许一个进程使用的共享资源
2.2 锁膨胀
如果在一个循环中,频繁的获取和释放做资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。
public void method(){
for(int i = 0;i < 999999;i++){
synchronized(对象){
}
}
// 这是上面的代码会触发锁膨胀
synchronized(对象){
for(int i = 0;i < 999999;i++){
}
}
}
2.3 锁升级
锁升级:ReentrantLock
的实现,是先基于乐观锁的 CAS
尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized
在JDK1.6
之前,完全就是获取不到锁,立即挂起当前线程,所以 synchronized
性能比较差。
我们简单介绍一下锁升级的步骤,后面会详细介绍
- 无锁、匿名偏向:当前对象没有作为锁存在。
- 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程 。
- 如果是,直接拿着锁资源走。
- 如果当前线程不是,基于
CAS
的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)
- 轻量级锁:会采用自旋锁的方式去频繁的以
CAS
的形式获取锁资源(采用的是自适应自旋锁)- 如果成功获取到,拿着锁资源走
- 如果自旋了一定次数,没拿到锁资源,锁升级。
- 重量级锁:就是最传统的
synchronized
方式,拿不到锁资源,就挂起当前线程。(用户态&内核态)
四、synchronized实现原理
4.1 字节码
我们从字节码层面解析一下 synchronized
做了什么
我们将上述代码:
public class SynchronizedTest {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
synchronized (o) {
System.out.println("111");
}
}
}
编译成 Class
文件,然后使用 javap -c SynchronizedTest.class
得到编译后的字节码文件
public class cn.hls.SynchronizedTest {
public cn.hls.SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
Code:
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter // 【重点】
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #4 // String 111
17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: aload_2
21: monitorexit // 【重点】
22: goto 30
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
}
JVM
保证一个 monitor
一次只能被一个线程占有,monitorenter
和 monitorexit
是两个与监视器相关的字节码指令。
当线程执行 monitorenter
的时候尝试获取栈顶对象的监视器 monitor
,也就是尝试获取锁,如果此时 monitor
没有被其他线程占用就获得锁,monitor
计数器设置为1,当线程已经获得 monitor
的所有权了,monitorenter
指令也会顺利执行,monitor
计数器+1.如果其他线程拥有 monitor
的所有权,当前线程会阻塞,直到 monitor
计数器变为0。
当线程执行 monitorexit
指令的时候,监视器计数器-1,计数器为0的时候锁被释放,其他等待的线程可以尝试获得 monitor
的所有权。
这里如果加的是方法锁,反编译获取字节码会得到 ACC_SYNCHRONIZED
的标记,这个标记在我们的 HotSpot
里面也会隐式的调用 monitorenter
和 monitorexit
。
至于这里的 monitorenter
和 monitorexit
底层原理如何,我们后续再讲。
4.2 Mark Word
在我们讲述 synchronized
原理之前,我们需要对对象的结构做一个大概的描述,毕竟 synchronized
是基于对象的。
这里我们主要关心对象头中的 Mark Word
大家可以停个几分钟,观察一下这个图
从上图我们基本可以猜测到,synchronized
关键字的实现,就是在对象的 Mark Word
里面做了一系列的标记,从而实现的锁升级。
五、深入Mark Word探寻锁升级
为了能在 IDEA
看到对象头的信息,我们需要导入下面的 maven
包:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
1、无锁
无锁状态是我们最经典的状态:
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
如上,我们打印出其 MarkWord
构成:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我们可以看到,其数值为:00000001 00000000 00000000 00000000
,也就是 001
无锁状态。
1.1 偏向锁延迟
这里我们加上 synchronized
关键字
public class SynchronizedTest {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
如上,我们打印出其 MarkWord
构成:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 48 f4 aa 02 (01001000 11110100 10101010 00000010) (44758088)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我们可以看到,其数值为:01001000 11110100 10101010 00000010
,也就是 000
轻量级锁状态。
这个时候可能有一些懵逼,怎么直接跳到轻量级锁了,不是说好的偏向锁呢
这里需要介绍一个技术,叫做:偏向锁延迟
偏向锁在升级为轻量级锁时,会涉及到偏向锁撤销,需要等到一个安全点(STW),才可以做偏向锁撤销,在明知道有并发情况,就可以选择不开启偏向锁,或者是设置偏向锁延迟开启
因为JVM在启动时,需要加载大量的.class文件到内存中,这个操作会涉及到synchronized的使用,为了避免出现偏向锁撤销操作,JVM启动初期,有一个延迟4s开启偏向锁的操作
说人话:当我们的 JVM 启动的时候,偏向锁的撤销必须要等到一个安全点(STW),但这个安全点的出现会导致系统效率的低下,于是在启动的时候暂时不开启偏向锁,等到 4s 之后再开启偏向锁。
1.2 匿名偏向锁
当我们将上述代码改为如下:
public class SynchronizedTest {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(6000);
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
如上,我们打印出其 MarkWord
构成:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
你神奇的看到,在我们没有加 synchronized
的情况下,他竟然变成了 101
偏向锁。
原因:如果正常开启偏向锁了,那么不会出现无锁状态,对象会直接变为匿名偏向
注意:这里的匿名偏向锁是没有存储线程的!
2、偏向锁
不论是由无锁状态升级到的偏向锁,还是匿名偏向锁
在偏向锁这一步做的功能也很简单,就是 MarkWord
保存了你当前线程的 ID
如果当前线程再次获取锁资源时,会从 MarkWord
中拉取到当前的线程 ID
并进行对比,若一致则放行
否则发生锁竞争,将当前的偏向锁升级为轻量级锁
2.1 偏向锁的撤销
偏向锁撤销的开销花费还是挺大的,其大概过程如下:
- 在一个安全点停止拥有锁的线程。
- 遍历线程的栈帧,检查是否存在锁记录。如果存在锁记录,就需要清空锁记录,使其变成无锁状态,并修复锁记录指向的
Mark Word
,清除其线程ID。 - 将当前锁升级成轻量级锁。
- 唤醒当前线程。
所以,如果某些临界区存在两个及两个以上的线程竞争,那么偏向锁反而会降低性能。在这种情况下,可以在启动 JVM
时就把偏向锁的默认功能关闭。这也就是上面我们 偏向锁延迟
的原因
3、轻量级锁
当升级至轻量级锁时,这个时候会有一个变化:
从我们的栈帧中开辟一块内存,叫做:Lock Record,这个东西是做什么用的呢?
3.1 Lock Record的作用
当我们的线程抢占一个轻量级锁时,这个时候会自动在栈帧里面开辟一块空间:Lock Record
,这个空间里面有两个参数:
- owner:当前对象
- Displaced Mark Word:当前对象的 Mark Word(这里会复制一份)
如果当前线程可以成功抢占该锁,对象的锁记录指针指向我们的 Lock Record
,而 owner
则指向当前的 Mark Word
整体流程:
- 虚拟机在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的
Mark Word
复制到锁记录中。 - 拷贝成功后,虚拟机将使用CAS操作尝试将对象的
Mark Word
更新为指向Lock Record
的指针,并将Lock Record
里的owner
指针指向对象的Mark Word
。 - 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象
Mark Word
的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。 - 如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
3.2 为什么需要开辟 Lock Record?
当一个对象被调用 HashCode()
方法时,JVM 会将该对象的 HashCode
存储到对象头中,保证每次调用的都一致,这时候无锁状态下是没有问题的。
但如果锁升级到偏向锁,HashCode
在对象头里面没有空间去存储了,所以这也是偏向锁不能和 HashCode
共同存在的原因。
所以,JVM
推出了轻量级锁的优化,在线程中开辟了 Lock Record
内存,里面存储着 Mard Word 的信息,不断的 CAS 请求对象头的线程指向。
如果可以成功,将 Mard Word
内的指针指向当前线程的 Lock Record
就OK了。
当最终完成之后,对象头撤销到无锁状态,这时候只需要将 Lock Record
再赋值过去就好了。
3.3 普通自旋锁 和 自适应自旋锁
普通自旋锁: 每次自旋次数是固定的,只有超过这个次数之后,才升级为重量级锁
自适应自旋锁:每次自旋的次数不是固定的,是基于上一次抢占到锁自旋的次数,由 JVM
自适应的去调整的
4、重量级锁
终于到了我们最后的重量级锁,从上面我们也应该可以看出来,重量级锁主要是基于 Monitor
那么我们直接看 HotSpot
关于 Monitor
的实现:
博主认为,你可以不懂源码,但这两个类一定要记住(面试吹牛使用)!
首先,我们看一下他的结构体:
//Monitor结构体
ObjectMonitor::ObjectMonitor() {
_header = NULL;
// 用来该线程获取锁的次数
_count = 0;
// 等待中的线程数
_waiters = 0,
//线程的重入次数
_recursions = 0;
_object = NULL;
//标识拥有该Monitor的线程
_owner = NULL;
//等待线程组成的双向循环链表
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
//多线程竞争锁进入时的单向链表
cxq = NULL ;
FreeNext = NULL ;
//_owner从该双向循环链表中唤醒线程节点
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
这里我们只需要记住以下这几个就可以:
- cxq:竞争队列(Contention Queue),所有请求锁的线程首先被放在这个竞争队列中。
- EntryList:Cxq中那些有资格成为候选资源的线程被移动到
EntryList
中。 - _WaitSet:某个拥有
ObjectMonitor
的线程在调用Object.wait()
方法之后将被阻塞,然后该线程将被放置在WaitSet
链表中。 - _owner:标识拥有该Monitor的线程
- _recursions:线程的重入次数(这个其实用处不大,写在这里主要是面试重入锁考的较多)
4.1cxq
在线程进入 Cxq
前,抢锁线程会先尝试通过 CAS
自旋获取锁,如果获取不到,就进入 Cxq
队列,这明显对于已经进入 Cxq
队列的线程是不公平的。所以,synchronized
同步块所使用的重量级锁是不公平锁。
4.2 EntryList
在 Owner
线程释放锁时,JVM
会从 Cxq
中迁移线程到 EntryList
,并会指定 EntryList
中的某个线程(一般为Head)为 OnDeck Thread
(Ready Thread)
EntryList
中的线程作为候选竞争线程而存在。
这里大家可能有个疑问,为什么我们需要把锁竞争交给 OnDeck Thread
简单来说,这里是为了提升系统整体的吞吐量,大家这里想象一下 Kafka
大概就能懂了。
4.3 _WaitSet
如果 Owner
线程被 Object.wait()
方法阻塞,就转移到 WaitSet
队列中,直到某个时刻通过 Object.notify()
或者Object.notifyAll()
唤醒,该线程就会重新进入 EntryList
中。
4.4 整体流程
- 我们线程刚进来时,会进入
Cxq
的队列中 - 当我们的
owner
释放锁时,会将Xcq
里面的线程放到EntryList
中 - 这个时候由
OnDeck Thread
去进行锁竞争,竞争失败的则继续留在EntryList
中 - 当调用
Object.wait()
会进入_WaitSet
队列,只要被唤醒时,才会重新进入EntryList
中
在重量级锁中没有竞争到锁的对象会 park
被挂起,退出同步块时 unpark
唤醒后续线程。唤醒操作涉及到操作系统调度会有额外的开销。
六、深入 HotSpot 探寻锁升级
这里博主的技术有限,就不自己写了,搬了搬小米技术博客的文章:xiaomi-info.github.io/2020/03/24/…
6.1 monitor 竞争过程
- 通过
CAS
尝试把monitor
的owner
字段设置为当前线程。 - 如果设置之前的
owner
指向当前线程,说明当前线程再次进入monitor
,即重入锁执行recursions ++
, 记录重入的次数。 - 如果当前线程是第一次进入该
monitor
, 设置recursions
为 1,_owner
为当前线程,该线程成功获得锁并返回。 - 如果获取锁失败,则等待锁的释放。
执行 monitorenter
指令时 调用以下代码
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),"must be NULL or an object");
// 是否使用偏向锁 JVM 启动时设置的偏向锁-XX:-UseBiasedLocking=false/true
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
// 轻量级锁
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
slow_enter
方法主要是轻量级锁的一些操作,如果操作失败则会膨胀为重量级锁,过程前面已经描述比较清楚此处不在赘述。enter
方法则为重量级锁的入口源码如下
void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD ;
void * cur ;
// 省略部分代码
// 通过 CAS 操作尝试把 monitor 的_owner 字段设置为当前线程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
return ;
}
// 线程重入,recursions++
if (cur == Self) {
_recursions ++ ;
return ;
}
// 如果当前线程是第一次进入该 monitor, 设置_recursions 为 1,_owner 为当前线程
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
for (;;) {
jt->set_suspend_equivalent();
// 如果获取锁失败,则等待锁的释放;
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;
jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}
}
6.2 monitor 等待
- 当前线程被封装成
ObjectWaiter
对象node
,状态设置成ObjectWaiter::TS_CXQ
。 for
循环通过CAS
把node
节点push
到_cxq
列表中,同一时刻可能有多个线程把自己的node
节点push
到_cxq
列表中。node
节点push
到_cxq
列表之后,通过自旋尝试获取锁,如果还是没有获取到锁则通过park
将当前线程挂起等待被唤醒。- 当该线程被唤醒时会从挂起的点继续执行,通过
ObjectMonitor::TryLock
尝试获取锁。
// 省略部分代码
void ATTR ObjectMonitor::EnterI (TRAPS) {
Thread * Self = THREAD ;
// Try lock 尝试获取锁
if (TryLock (Self) > 0) {
// 如果获取成功则退出,避免 park unpark 系统调度的开销
return ;
}
// 自旋获取锁
if (TrySpin(Self) > 0) {
return;
}
// 当前线程被封装成 ObjectWaiter 对象 node, 状态设置成 ObjectWaiter::TS_CXQ
ObjectWaiter node(Self) ;
Self->_ParkEvent->reset() ;
node._prev = (ObjectWaiter *) 0xBAD ;
node.TState = ObjectWaiter::TS_CXQ ;
// 通过 CAS 把 node 节点 push 到_cxq 列表中
ObjectWaiter * nxt ;
for (;;) {
node._next = nxt = _cxq ;
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
// 再次 tryLock
if (TryLock (Self) > 0) {
return ;
}
}
for (;;) {
// 本段代码的主要思想和 AQS 中相似可以类比来看
// 再次尝试
if (TryLock (Self) > 0) break ;
assert (_owner != Self, "invariant") ;
if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}
// 满足条件则 park self
if (_Responsible == Self || (SyncFlags & 1)) {
TEVENT (Inflated enter - park TIMED) ;
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
RecheckInterval *= 8 ;
if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
TEVENT (Inflated enter - park UNTIMED) ;
// 通过 park 将当前线程挂起,等待被唤醒
Self->_ParkEvent->park() ;
}
if (TryLock(Self) > 0) break ;
// 再次尝试自旋
if ((Knob_SpinAfterFutile & 1) && TrySpin(Self) > 0) break;
}
return ;
}
6.3 monitor 释放
当某个持有锁的线程执行完同步代码块时,会释放锁并 unpark
后续线程(由于篇幅只保留重要代码)。
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
Thread * Self = THREAD ;
if (_recursions != 0) {
_recursions--; // this is simple recursive enter
TEVENT (Inflated exit - recursive) ;
return ;
}
ObjectWaiter * w = NULL ;
int QMode = Knob_QMode ;
// 直接绕过 EntryList 队列,从 cxq 队列中获取线程用于竞争锁
if (QMode == 2 && _cxq != NULL) {
w = _cxq ;
ExitEpilog (Self, w) ;
return ;
}
// cxq 队列插入 EntryList 尾部
if (QMode == 3 && _cxq != NULL) {
w = _cxq ;
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}
ObjectWaiter * Tail ;
for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ;
if (Tail == NULL) {
_EntryList = w ;
} else {
Tail->_next = w ;
w->_prev = Tail ;
}
}
// cxq 队列插入到_EntryList 头部
if (QMode == 4 && _cxq != NULL) {
// 把 cxq 队列放入 EntryList
// 此策略确保最近运行的线程位于 EntryList 的头部
w = _cxq ;
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}
if (_EntryList != NULL) {
q->_next = _EntryList ;
_EntryList->_prev = q ;
}
_EntryList = w ;
}
w = _EntryList ;
if (w != NULL) {
ExitEpilog (Self, w) ;
return ;
}
w = _cxq ;
if (w == NULL) continue ;
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
if (QMode == 1) {
// QMode == 1 : 把 cxq 倾倒入 EntryList 逆序
ObjectWaiter * s = NULL ;
ObjectWaiter * t = w ;
ObjectWaiter * u = NULL ;
while (t != NULL) {
guarantee (t->TState == ObjectWaiter::TS_CXQ, "invariant") ;
t->TState = ObjectWaiter::TS_ENTER ;
u = t->_next ;
t->_prev = u ;
t->_next = s ;
s = t;
t = u ;
}
_EntryList = s ;
assert (s != NULL, "invariant") ;
} else {
// QMode == 0 or QMode == 2
_EntryList = w ;
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
// 将单向链表构造成双向环形链表;
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}
}
if (_succ != NULL) continue;
w = _EntryList ;
if (w != NULL) {
guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
ExitEpilog (Self, w) ;
return ;
}
}
}
6.4 notify 唤醒
notify
或者 notifyAll
方法可以唤醒同一个锁监视器下调用 wait
挂起的线程,具体实现如下
void ObjectMonitor::notify(TRAPS) {
CHECK_OWNER();
if (_WaitSet == NULL) {
TEVENT (Empty - Notify);
return;
}
DTRACE_MONITOR_PROBE(notify, this, object(), THREAD);
int Policy = Knob_MoveNotifyee;
Thread::SpinAcquire(&_WaitSetLock, "WaitSet - notify");
ObjectWaiter *iterator = DequeueWaiter();
if (iterator != NULL) {
// 省略一些代码
// 头插 EntryList
if (Policy == 0) {
if (List == NULL) {
iterator->_next = iterator->_prev = NULL;
_EntryList = iterator;
} else {
List->_prev = iterator;
iterator->_next = List;
iterator->_prev = NULL;
_EntryList = iterator;
}
} else if (Policy == 1) { // 尾插 EntryList
if (List == NULL) {
iterator->_next = iterator->_prev = NULL;
_EntryList = iterator;
} else {
ObjectWaiter *Tail;
for (Tail = List; Tail->_next != NULL; Tail = Tail->_next);
assert (Tail != NULL && Tail->_next == NULL, "invariant");
Tail->_next = iterator;
iterator->_prev = Tail;
iterator->_next = NULL;
}
} else if (Policy == 2) { // 头插 cxq
// prepend to cxq
if (List == NULL) {
iterator->_next = iterator->_prev = NULL;
_EntryList = iterator;
} else {
iterator->TState = ObjectWaiter::TS_CXQ;
for (;;) {
ObjectWaiter *Front = _cxq;
iterator->_next = Front;
if (Atomic::cmpxchg_ptr(iterator, &_cxq, Front) == Front) {
break;
}
}
}
} else if (Policy == 3) { // 尾插 cxq
iterator->TState = ObjectWaiter::TS_CXQ;
for (;;) {
ObjectWaiter *Tail;
Tail = _cxq;
if (Tail == NULL) {
iterator->_next = NULL;
if (Atomic::cmpxchg_ptr(iterator, &_cxq, NULL) == NULL) {
break;
}
} else {
while (Tail->_next != NULL) Tail = Tail->_next;
Tail->_next = iterator;
iterator->_prev = Tail;
iterator->_next = NULL;
break;
}
}
} else {
ParkEvent *ev = iterator->_event;
iterator->TState = ObjectWaiter::TS_RUN;
OrderAccess::fence();
ev->unpark();
}
if (Policy < 4) {
iterator->wait_reenter_begin(this);
}
}
// 自旋释放
Thread::SpinRelease(&_WaitSetLock);
if (iterator != NULL && ObjectMonitor::_sync_Notifications != NULL) {
ObjectMonitor::_sync_Notifications->inc();
}
}
6.5 总结
说实话,看了这几个源码,感觉作用不太大.....
面试能吹到源码这一步,也是你牛逼了
个人感觉,记住上面的流程,再记住 park
和 unpark
就够了
LockSupport 的 park 和 unpark 是依赖 JVM(此处语境讨论 Hotspot)调用操作系统的 pthread_mutex_lock 和 pthread_cond_wait , 前者是保护后者和 counter 变量的互斥锁,保证只有一个线程操作 counter 变量和 condtion 上的等待队列
七、流程图
八、总结
又是一篇大工程的文章结束了
记得校招的时候,知道一个锁升级,就感觉自己已经无敌了,转眼间,重新整理了一遍才发现 synchronized
这哥们这么难
但通过这篇文章,我相信,99% 的人应该都可以理解了 synchronized
的实现
没理解最后 HotSpot
源码的也不要灰心,因为博主也没理解......
那么如何证明你真的理解了 synchronized
呢,我这里出个经典的题目,大家可以想一下:说说synchronized锁升级的过程?
如果你能看到这,那博主必须要给你一个大大的鼓励,谢谢你的支持