本篇将从源码角度,了解竞争重量级锁失败的过程,解释了为什么t2启动后,立马调用getState方法,会显示处于RUNNABLE状态,也说明了在Java线程中的Blocked状态并非是完全挂起状态,入队_cxq前后都可能进行TryLock()和自旋。
竞争重量级锁之前
稍微熟悉synchronized就会知道,Java编译器在编译带有该关键字的代码块后,会插入monitorenter以及monitorexit指令到字节码中,monitorenter也就是进入内置锁机制的入口,看看虚拟机内部的部分实现:
|
|
为了聚焦问题,所以上一篇已经强调过本系列主要研究的对象是重量级锁,获取轻量级锁失败后会去调用InterpreterRuntime::monitorenter方法,继续追踪有:
|
|
fast_enter方法的作用注释已经充分的说明:
Retry fast entry if bias is revoked to avoid unnecessary inflation
所以主要看slow_enter方法,毕竟本人关注的是获取重量级锁的机制:
|
|
可见在正式竞争重量级锁之前,还会有一个锁膨胀的过程,当调用enter方法后,就正式开始竞争重量级锁了。
本小节跳过了无锁、偏向锁、轻量级锁以及锁膨胀的实现,但在追踪代码的过程中,对竞争重量级锁之前的流程有了大致的了解。
t2的失败之旅
延用前篇最后列举的例子,看看t2是如何获取锁未遂的。
ObjectMonitor::enter()
ObjectMonitor::enter()可以说是真正开始竞争重量级锁的入口,当t2启动后尝试去拿锁,此时t1持有锁并处在操作系统的IO阻塞状态,这是一个竞争重量级锁的过程(在此之前会通过一系列判断后膨胀锁),ObjectMonitor::enter一开始进行是否重入的检查:
|
|
cmpxchg_ptr (Self, &_owner, NULL)的规则:
- &_owner和NULL相等,则将Self指向的内容赋给&_owner,返回NULL;
- &_owner和NULL不相等,则返回_owner;
第一个if语句用来处理CAS返回NULL的情形,此情形说明_owner指向的内容为空,没有线程持有锁,CAS将_owner指向当前线程,进行断言判断后直接返回;
第二个if语句用来处理其它的情形,&_owner和NULL不相等会返回_owner,此时如果_owner == Self,说明线程重入,那么重入游标_recursions将自增。
接下来的代码涉及到之前锁膨胀的内容:
|
|
之前已经说明,竞争重量级锁之前的“故事”暂不深入。这里大致思想应该是轻量级锁膨胀为重量级锁后,若获得了轻量级锁又再次获得重量级锁,还是相当于锁重入了,所以_recursions设置为1,表示重入1次。
紧接着有:
|
|
自旋拿锁失败,可用断言验证此时Java线程的状态不为blocked状态:
|
|
所以t2启动后一开始处于runnable状态
。
|
|
上面的代码将线程状态设置为blocked状态,此时没有拿到monitor锁,也没有在任何队列,不会突然的被JVMTI_EVENT_MONITOR_CONTENDED_ENTER事件unpark()。
后续会出现for循环:
|
|
这里先关注EnterI (THREAD)。
ObjectMonitor::EnterI()
该方法是最终没有拿到锁的线程入队的关键方法,但在入队前后,还会不停尝试获取锁。
进入该方法首先会断言判断线程状态为_thread_blocked,所以进入该方法时的状态确定是blocked状态,接着还会TryLock()和一轮TrySpin()。在通过断言确定没有拿到锁之后,开始入队操作:
|
|
可见,方法将线程包装成一个ObjectWaiter对象,继续下面的代码:
|
|
cmpxchg_ptr()之前已经解析过了,在这里,当_cxq指向内容和nxt的相等时,node就赋给_cxq,完成入队并跳出循环;否则继续TryLock(),没拿到锁就循环再次尝试入队。将关键代码再单独拿出来:
|
|
这里的逻辑表明,当有新来的node时,它的next总是指向当前头节点,而当cmpxchg_ptr成功后,头节点指针_cxq就会指向新来的node,所以后来者当头
。
此时如若仍然没有获得锁,将进入最后的一个for循环中,这里列出主要代码:
|
|
代码中,每次循环中会进行数次TryLock()尝试拿锁,期间会执行Self->\_ParkEvent->park() ;
将自己挂起。
小结
通过本文可以知道一个线程在拿锁失败的过程中是如何切换Java状态的。在解读源码的过程中,弄清楚了cmpxchg_ptr()的比较规则,而后续文章将深入队列的唤醒机制。