-
Notifications
You must be signed in to change notification settings - Fork 288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
死磕Synchronized底层实现--偏向锁 #13
Comments
请问一下作者,文中偏向锁的撤销小节中,当线程A已经退出同步代码块了,此时线程B请求对象锁触发偏向锁撤销流程,锁对象根据是否允许重偏向被撤销为匿名偏向状态或无锁状态,若允许重偏向,线程B能直接获取到偏向锁吗,是否允许重偏向是如何判断的呢,期待解惑,万分感谢 |
感谢解答,文章写得很好很清楚,基本上理解了,但是还有一点点疑问,在
但是此处只是将锁对象的mark word设置为匿名偏向状态,而未通过CAS将偏向线程设置为当前B线程的ID,这个逻辑是在哪里实现的呢 |
@yinjk 先回答第二个问题。
非常抱歉,我上面的回答中关于重偏向的代码引用有误。 下面为正确答案: 对于B线程来说,逻辑在这里 对于其他case来说,是在下一次获得锁时发生([这里])(http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/runtime/biasedLocking.cpp#l576)。 |
@yinjk 关于第一个问题,应该是说:有且只有调用 |
非常感谢作者耐心解答,根据指示,问题已经得到解决。 |
感谢,最近一直在学习偏向锁方面的知识,自己也看了源码还是无法理解一些细节,拜读了此文之后疑问全解,感觉浑身通透 |
@zyllt 有帮助就好~ |
有个疑问,如果没有达到批量重偏向应该执行的是单个重偏向,这里直接返回了BiasedLocking::BIAS_REVOKED,没有让B线程去竞争偏向锁。猜测可能是
|
@zyllt 对于非批量重偏向的步骤如下: 2.在之后的slow_enter中,如果锁对象是无锁状态,则会升级为轻量级锁。否则升级为重量级锁。 也就是说,如果一个锁已经偏向线程A,当线程B尝试获得该锁时,无论线程A是什么状态,该锁都会升级成轻量级锁或重量级锁(不考虑批量重偏向的情况)。 如果你还不太理解,建议先看下上篇文章补充下概念上的知识。 |
@farmerjohngit 感谢回答,确实是我理解错了。 |
您的文章写的非常详细,受教了!我有一个问题,始终没有明白,在偏向锁入口code10,为什么在这个逻辑里处理偏向锁重入 |
@ValiantYuan 走到这里说明已经是轻量级锁的逻辑了 |
谢谢,明白了。 |
您好,我想请教一下,假如偏向锁持所线程Thread A已经不存活了,Thread B触发VMThread将偏向锁重置为匿名偏向后,Thread B在又何时重新CAS获取到偏向锁? |
是指只要Thread B在HR_SINGLE_REVOKE case下,因为revoke_bias()方法传的allow_rebias都是false,所以不会重偏向
是指只要Thread B在HR_SINGLE_REVOKE case下,因为revoke_bias()方法传的allow_rebias都是false,所以不会重偏向? 还有几个问题也想一并请教一下
|
@DanFL 最近比较忙,你的问题我慢慢回答哈
是 |
epoch的作用是在触发批量重偏向阈值后,下次获得锁时,通过判断class的epoch和obj的epoch决定要不要直接重偏向。 这点在本文中也说过了,建议你将本文开头处的 |
这里我写的有问题,是重偏向失败才进入 |
如果你说的是Java层的类对象,那是的,因为类对象本身也是对象。至于你问的区别,markword是对象的(包括类对象),Native层对应的是 如果你说的是Native层的Klass,那不是的,klass.hpp中并没有 |
水平不够 看源码实在是受不住啊 全靠注释看下来了 有个小问题 在锁升级的时候:
偏向锁重入也会插入一次Lock Record嘛?那为什么上面偏向锁重入那里的注释是什么也没做呢? |
|
不太记得了 不排除我搞错了 |
如果要撤销的锁偏向的不是当前线程,会将该操作push到VM Thread中等到safepoint的时候再执行。 |
@May0302 在VM_RevokeBias的模式下,VMThread::execute是阻塞的 |
如果一个系统都用object类作为锁,是不是很可能导致这个object锁被撤销? |
偏向锁重入时,添加一个lock Record,默认给lock Record.lock()->set_displaced_header(),分配一个空的值。对象没有赋值的情况下,如果是int默认值为0,如果是一个对象默认值为null。至于轻量锁重入时,在判断轻量锁重入之前,进行CAS操作,会把一个无锁的001mark word 赋值给lock Record->lock()->set_displaced_header(‘.....001’),因为是轻量锁重入,当前线程是持有一个t线程,所以加锁不成功。之前设置的lock Record->lock()->set_displaced_header(‘.....001’),要把它恢复成null。
|
在阅读偏向锁的源码后,自己对知识做了测试验证,其中下面有一个理解上面的确认 和 还有一个没有理解清楚的疑问,下面先说明一下测试的情况:
I,在code1 没有代码的代码时候,上面code2打印是轻量级锁状态000。 这个状态我的理解1:这个按照源码的推演,在执行第二个synchronized时:应该是批量重偏向的阈值还未达到,所以先阻塞进入安全点->再修改成无锁状态,->最终膨胀成轻量级锁(ObjectSynchronizer::slow_enter中在)。 II,如果在code1出加入 System.gc()后,则在code 2 打印的锁状态为偏向锁101。 这个状态我的理解2:这里为什么手动触发一次fullgc就会重偏向呢? 我猜测fullgc会进入全局安全点会将class的epoch+1,但是锁对象的不变(没有找到代码验证此猜测,也是我疑惑的点),导致2者不一致。最终在执行第二个synchronized时,由于class和锁实例的epoch不一致,会触发重偏向新线程。源码 第二个测试验证:
上面代码改成2个独立的线程。 这里却为什么 在code3 这里却是打印为偏向锁状态(等于发生了单次重偏向)。 我的理解3: 未知!!! 这里按理跟 ‘理解1’那里的情况是一样,应该也是 轻量级锁状态,因为lockObj都不在同步块,并且前面已经有过偏向,而且有没有出现epoch不一致的情况,这里为何就是 做了一次重偏向呢? 我用的jdk1.9测试的。 请教一下几个疑问, |
@xdcode2020
至于第二个例子,可能会出现,这样的情况,CPU使用第一个线程 t1 后,结束后,代码在启动的线程可能还是t1。并不是重新启动另一个线程。但也可能不会出现这样的情况。根据时间片轮转,可能也不会出现这个问题。看机率。 |
使用博主你的代码,我测试: 在JDK9时:加了GC后,第二个同步块的锁为:101偏向锁(跟博主结果不一样), 没有加GC后,就是轻量级锁(跟博主结果一样),有点疑惑了 . 在用jdk8(jdk1.8.0_181,这个为非JDK8U版本吧)不管加不加GC,都是轻量级锁。 ps:我为什么要用JDK9,是因为看源码就如博主所说jdk8u和jdk8 偏向锁的源码存在差别,在核对与jdk8u以上的版本是一致的(特意对比了一下 jdk9 和jdk8u的几个偏向锁的核心函数的源码,逻辑都是一样的,只是打印日志做了些调整),所以采用JDK9也进行了一次对比测试。 想向博主和大家先确定一下这个认识是否正确? 按照jdk8 偏向锁的源码,如果第一个线程A偏向后(重偏向阈值未到达,并且第一个线程A已跳出同步块),另外一个线程B再进入同步块,中我阅读的理解是不会重偏向,而是升级为同步锁,这个理解是否正确? 以上理解的源码调用栈分析依据: 第一步:revoke_and_rebias函数,因为class和对象的epoch是一致的(之前没有进入过安全点),所以会走这个分支阻塞等待进入安全点: 源码 第二步:安全点到达后,最终会调用revoke_bias函数,因为main线程存活,但已经跳出同步块,所以会走这个设置无锁状态:源码二 第三步:进入ObjectSynchronizer::slow_enter中升级成轻量级锁。 👇👇👇 JVM配置:-XX:BiasedLockingStartupDelay=0 关闭偏向锁延迟效果 2个测试的jdk版本: 测试代码(使用的博主的): static LockObj lockObj = new LockObj();
static Thread t1;
public static void main(String[] args) throws InterruptedException {
// Thread.sleep(10000);
System.out.println("1:" + ClassLayout.parseInstance(lockObj).toPrintable());
synchronized (lockObj) {
System.out.println("main :" + ClassLayout.parseInstance(lockObj).toPrintable());
}
System.gc(); //下面有加与不加 这行代码的测试结果.
t1 = new Thread(() -> {
synchronized (lockObj) {
System.out.println("t1 :" + ClassLayout.parseInstance(lockObj).toPrintable());
}
});
t1.start();
t1.join();
System.out.println("main end :" + ClassLayout.parseInstance(lockObj).toPrintable());
} 加了System.gc(); JDK9的输出:
JDK8的输出:
不加System.gc(); JDK9的输出:
JDK8的输出:
|
@wangliyu0328 |
@wangliyu0328 同样的疑惑,怀疑是不是Cas修改了record后也会同时修改锁状态?我还提了一个类似的问题 |
@xdcode2020 |
@scn7th 无锁:没有使用:25位,hash:31位 ,没有使用:1位 GC年龄:4位 偏向标识:1 锁状态:01 第三个问题 轻量级锁解锁后,用cas替换对象的markword为栈帧中的displaced mark word,此时displaced mark word应该是锁升级前的,那么此时轻量级锁是不是就完全释放下一次,加锁前是偏向/无锁状态,而不是直接进入轻量级锁?这样不就是锁降级了? |
@baixinping0 升级为轻量锁前,就是把Displaced header设置为锁状态的 01,轻量级锁释放的时候,对象头变成01(无锁)了。轻量锁升级的时候,CAS把锁标识设置为00。但轻量锁释放的时候,为01的。 |
|
@aLibeccio |
@farmerjohngit 您好,我有一个疑问就是
这里从klass的prototype_header中获取了一些信息,来判断偏向锁是否指向当前线程,epoch是否过期等,但是等偏向锁指向当前线程
后,并没有看见更新klass的prototype_header更新,那偏向锁重入就会有问题,请问prototype_header是在哪里更新的? |
我也看了下这块的代码,可以参考下这个:https://github.com/HenryChenV/my-notes#synchronized%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90 |
谢谢答疑。
我的疑问是,偏向锁偏向某个线程时不会修改prototype_header那么重入时下面的分支语句该怎么走? if (anticipated_bias_locking_value == 0) {
//分支1: 不会进入到这里,因为偏向锁偏向某个线程时不会修改prototype_header并没有指向当前线程,
prototype_headeranticipated_bias_locking_value !=0
...
}
else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
// 分支2
...
}
else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
// 分支3
...
}
else {
// 分支4: 走到这里说明当前要么偏向别的线程,要么是匿名偏向(即没有偏向任何线程)
...
} 在没有修改prototype_header的时候,分支1肯定不会走到,而分支4按楼主描述是处理匿名偏向的,那会走到分支3还是分支2? |
你好,有一个小的细节问题,想要请教一下。在锁撤销部分的偏向标志被关闭时的代码(就是这段 简单来说,问题就是:在偏向模式关闭,则尝试撤销偏向锁时,为什么要替换lockee markword的,是class中的mark word,而不是构造一个无锁的header? 还望赐教。 |
@DanchuoZhong 偏向锁批量撤销 :当一个线程撤销次数20次时,会统一把这个对象 A a的类, 只要含有A这个类的锁,都撤销为无锁状态。CAS在构造header时需要去取prototype_header的相关信息,是去取类的信息,而不仅仅是对象a 的锁信息。lockeet->mark_addr()是取是对象的mark地址。 |
@XHxin 有一个场景:线程t1加锁然后释放(偏向锁),然后t2来加锁再释放(轻量锁),然后t3来加锁(轻量锁); |
开局就没看懂= = |
偏向锁Lock Record怎么生成的呀 |
请问代码注释里success==false的逻辑是不是有点问题?我的理解是只有对象头不是偏向锁状态或者class标记为不可偏向,才会导致success==false;如果偏向其他线程,虽然会调用monitorenter,但是success被修改成true,不会重新走success==false的流程 |
我也是这么想的,偏向锁获取那段代码code 9的success=false只有一种情况就是klass的prototype_header中是否偏向+锁标志不是101,这种情况就会走code 9普通轻量级锁的路线,偏向其他线程的话应该走的是code 8里CAS失败的else部分。。。 |
看了文章有两个地方一直困惑我,麻烦请教一下: |
必须给个👍 透彻! |
你好,有个疑问想请教下,当A,B线程同时获取锁时,假设当前锁对象是匿名可偏向的,A线程CAS成功了,获取到锁了,B线程失败了,就会进入这里CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);假设是开启了偏向并且没有触发批量重偏向和撤销,最终会启动VM线程执行撤销偏向锁吧,那启动VM线程接下来怎么执行呢?是执行这里UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);再次获取锁么?谢谢了 |
轻量级锁Lock record的指针地址和重量级锁Object monitor的指针地址都是00结尾的二进制吗?即4的倍数。 本人Java,不懂请教。 |
会执行到这一块代码, 具体的作者也有些 revoke的逻辑在fast_enter里 RT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
...
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
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);
}
...
IRT_END``` |
得到答案了吗? 我也有点懵逼 |
意思是这个情况已经不能说是重入锁了,此时已经变成了轻量级锁了吗,是前面对锁进行了升级这样的吗,还有,我不太清楚为什么后面会把displace work mard置为null,感觉不是很明白 |
1 similar comment
意思是这个情况已经不能说是重入锁了,此时已经变成了轻量级锁了吗,是前面对锁进行了升级这样的吗,还有,我不太清楚为什么后面会把displace work mard置为null,感觉不是很明白 |
本文为synchronized系列第二篇。主要内容为分析偏向锁的实现。
偏向锁的诞生背景和基本原理在上文中已经讲过了,强烈建议在有看过上篇文章的基础下阅读本文。
更多文章见个人博客:https://github.com/farmerjohngit/myblog
本系列文章将对HotSpot的
synchronized
锁实现进行全面分析,内容包括偏向锁、轻量级锁、重量级锁的加锁、解锁、锁升级流程的原理及源码分析,希望给在研究synchronized
路上的同学一些帮助。主要包括以下几篇文章:死磕Synchronized底层实现--概论
死磕Synchronized底层实现--偏向锁
死磕Synchronized底层实现--轻量级锁
死磕Synchronized底层实现--重量级锁
本文将分为几块内容:
1.偏向锁的入口
2.偏向锁的获取流程
3.偏向锁的撤销流程
4.偏向锁的释放流程
5.偏向锁的批量重偏向和批量撤销
本文分析的JVM版本是JVM8,具体版本号以及代码可以在这里看到。
偏向锁入口
目前网上的很多文章,关于偏向锁源码入口都找错地方了,导致我之前对于偏向锁的很多逻辑一直想不通,走了很多弯路。
synchronized
分为synchronized
代码块和synchronized
方法,其底层获取锁的逻辑都是一样的,本文讲解的是synchronized
代码块的实现。上篇文章也说过,synchronized
代码块是由monitorenter
和monitorexit
两个指令实现的。关于HotSpot虚拟机中获取锁的入口,网上很多文章要么给出的方法入口为interpreterRuntime.cpp#monitorenter,要么给出的入口为bytecodeInterpreter.cpp#1816。包括占小狼的这篇文章关于锁入口的位置说法也是有问题的(当然文章还是很好的,在我刚开始研究
synchronized
的时候,小狼哥的这篇文章给了我很多帮助)。要找锁的入口,肯定是要在源码中找到对
monitorenter
指令解析的地方。在HotSpot的中有两处地方对monitorenter
指令进行解析:一个是在bytecodeInterpreter.cpp#1816 ,另一个是在templateTable_x86_64.cpp#3667。前者是JVM中的字节码解释器(
bytecodeInterpreter
),用C++实现了每条JVM指令(如monitorenter
、invokevirtual
等),其优点是实现相对简单且容易理解,缺点是执行慢。后者是模板解释器(templateInterpreter
),其对每个指令都写了一段对应的汇编代码,启动时将每个指令与对应汇编代码入口绑定,可以说是效率做到了极致。模板解释器的实现可以看这篇文章,在研究的过程中也请教过文章作者‘汪先生’一些问题,这里感谢一下。在HotSpot中,只用到了模板解释器,字节码解释器根本就没用到,R大的读书笔记中说的很清楚了,大家可以看看,这里不再赘述。
所以
montorenter
的解析入口在模板解释器中,其代码位于templateTable_x86_64.cpp#3667。通过调用路径:templateTable_x86_64#monitorenter
->interp_masm_x86_64#lock_object
进入到偏向锁入口macroAssembler_x86#biased_locking_enter
,在这里大家可以看到会生成对应的汇编代码。需要注意的是,不是说每次解析monitorenter
指令都会调用biased_locking_enter
,而是只会在JVM启动的时候调用该方法生成汇编代码,之后对指令的解析是通过直接执行汇编代码。其实
bytecodeInterpreter
的逻辑和templateInterpreter
的逻辑是大同小异的,因为templateInterpreter
中都是汇编代码,比较晦涩,所以看bytecodeInterpreter
的实现会便于理解一点。但这里有个坑,在jdk8u之前,bytecodeInterpreter
并没有实现偏向锁的逻辑。我之前看的JDK8-87ee5ee27509这个版本就没有实现偏向锁的逻辑,导致我看了很久都没看懂。在这个commit中对bytecodeInterpreter
加入了偏向锁的支持,我大致了看了下和templateInterpreter
对比除了栈结构不同外,其他逻辑大致相同,所以下文就按bytecodeInterpreter中的代码对偏向锁逻辑进行讲解。templateInterpreter
的汇编代码讲解可以看这篇文章,其实汇编源码中都有英文注释,了解了汇编几个基本指令的作用再结合注释理解起来也不是很难。偏向锁获取流程
下面开始偏向锁获取流程分析,代码在bytecodeInterpreter.cpp#1816。注意本文代码都有所删减。
再回顾下对象头中mark word的格式:![image](https://camo.githubusercontent.com/2acfafe3660f02867b0012cc1f842c85c92ebca8cd25a6b44fa64c3a18ee8e8a/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31312f32382f313637353964643162306239363236383f773d37323026683d32353026663d6a70656726733d3337323831)
JVM中的每个类也有一个类似mark word的prototype_header,用来标记该class的epoch和偏向开关等信息。上面的代码中
lockee->klass()->prototype_header()
即获取class的prototype_header。code 1
,从当前线程的栈中找到一个空闲的Lock Record
(即代码中的BasicObjectLock,下文都用Lock Record代指),判断Lock Record
是否空闲的依据是其obj字段 是否为null。注意这里是按内存地址从低往高找到最后一个可用的Lock Record
,换而言之,就是找到内存地址最高的可用Lock Record
。code 2
,获取到Lock Record
后,首先要做的就是为其obj字段赋值。code 3
,判断锁对象的mark word
是否是偏向模式,即低3位是否为101。code 4
,这里有几步位运算的操作anticipated_bias_locking_value = (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) & ~((uintptr_t) markOopDesc::age_mask_in_place);
这个位运算可以分为3个部分。第一部分
((uintptr_t)lockee->klass()->prototype_header() | thread_ident)
将当前线程id和类的prototype_header相或,这样得到的值为(当前线程id + prototype_header中的(epoch + 分代年龄 + 偏向锁标志 + 锁标志位)),注意prototype_header的分代年龄那4个字节为0第二部分
^ (uintptr_t)mark
将上面计算得到的结果与锁对象的markOop进行异或,相等的位全部被置为0,只剩下不相等的位。第三部分
& ~((uintptr_t) markOopDesc::age_mask_in_place)
markOopDesc::age_mask_in_place为...0001111000,取反后,变成了...1110000111,除了分代年龄那4位,其他位全为1;将取反后的结果再与上面的结果相与,将上面异或得到的结果中分代年龄给忽略掉。code 5
,anticipated_bias_locking_value==0
代表偏向的线程是当前线程且mark word
的epoch等于class的epoch,这种情况下什么都不用做。code 6
,(anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0
代表class的prototype_header或对象的mark word
中偏向模式是关闭的,又因为能走到这已经通过了mark->has_bias_pattern()
判断,即对象的mark word
中偏向模式是开启的,那也就是说class的prototype_header不是偏向模式。然后利用CAS指令
Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark
撤销偏向锁,我们知道CAS会有几个参数,1是预期的原值,2是预期修改后的值 ,3是要修改的对象,与之对应,cmpxchg_ptr方法第一个参数是预期修改后的值,第2个参数是修改的对象,第3个参数是预期原值,方法返回实际原值,如果等于预期原值则说明修改成功。code 7,如果epoch已过期,则需要重偏向,利用CAS指令将锁对象的
mark word
替换为一个偏向当前线程且epoch为类的epoch的新的mark word
。code 8,CAS将偏向线程改为当前线程,如果当前是匿名偏向则能修改成功,否则进入锁升级的逻辑。
code 9,这一步已经是轻量级锁的逻辑了。从上图的
mark word
的格式可以看到,轻量级锁中mark word
存的是指向Lock Record
的指针。这里构造一个无锁状态的mark word
,然后存储到Lock Record
(Lock Record
的格式可以看第一篇文章)。设置mark word
是无锁状态的原因是:轻量级锁解锁时是将对象头的mark word
设置为Lock Record
中的Displaced Mark Word
,所以创建时设置为无锁状态,解锁时直接用CAS替换就好了。code 10, 如果是锁重入,则将
Lock Record
的Displaced Mark Word
设置为null,起到一个锁重入计数的作用。以上是偏向锁加锁的流程(包括部分轻量级锁的加锁流程),如果当前锁已偏向其他线程||epoch值过期||偏向模式关闭||获取偏向锁的过程中存在并发冲突,都会进入到
InterpreterRuntime::monitorenter
方法, 在该方法中会对偏向锁撤销和升级。偏向锁的撤销
这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块时的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别。
如果获取偏向锁失败会进入到InterpreterRuntime::monitorenter方法
可以看到如果开启了JVM偏向锁,那会进入到
ObjectSynchronizer::fast_enter
方法中。如果是正常的Java线程,会走上面的逻辑进入到
BiasedLocking::revoke_and_rebias
方法,如果是VM线程则会走到下面的BiasedLocking::revoke_at_safepoint
。我们主要看BiasedLocking::revoke_and_rebias
方法。这个方法的主要作用像它的方法名:撤销或者重偏向,第一个参数封装了锁对象和当前线程,第二个参数代表是否允许重偏向,这里是true。会走到该方法的逻辑有很多,我们只分析最常见的情况:假设锁已经偏向线程A,这时B线程尝试获得锁。
上面的
code 1
,code 2
B线程都不会走到,最终会走到code 4
处,如果要撤销的锁偏向的是当前线程则直接调用revoke_bias
撤销偏向锁,否则会将该操作push到VM Thread中等到safepoint
的时候再执行。关于VM Thread这里介绍下:在JVM中有个专门的VM Thread,该线程会源源不断的从VMOperationQueue中取出请求,比如GC请求。对于需要
safepoint
的操作(VM_Operationevaluate_at_safepoint返回true)必须要等到所有的Java线程进入到safepoint
才开始执行。 关于safepoint
可以参考下这篇文章。接下来我们着重分析下
revoke_bias
方法。第一个参数为锁对象,第2、3个参数为都为false需要注意下,当调用锁对象的
Object#hash
或System.identityHashCode()
方法会导致该对象的偏向锁或轻量级锁升级。这是因为在Java中一个对象的hashcode是在调用这两个方法时才生成的,如果是无锁状态则存放在mark word
中,如果是重量级锁则存放在对应的monitor中,而偏向锁是没有地方能存放该信息的,所以必须升级。具体可以看这篇文章的hashcode()方法对偏向锁的影响
小节(注意:该文中对于偏向锁的加锁描述有些错误),另外我也向该文章作者请教过一些问题,他很热心的回答了我,在此感谢一下!言归正传,
revoke_bias
方法逻辑:monitorenter
)的时候都会以从高往低的顺序在栈中找到第一个可用的Lock Record
,将其obj字段指向锁对象。每次解锁(即执行monitorexit
)的时候都会将最低的一个相关Lock Record
移除掉。所以可以通过遍历线程栈中的Lock Record
来判断线程是否还在同步块中。Lock Record
的Displaced Mark Word
设置为null,然后将最高位的Lock Record
的Displaced Mark Word
设置为无锁状态,最高位的Lock Record
也就是第一次获得锁时的Lock Record
(这里的第一次是指重入获取锁时的第一次),然后将对象头指向最高位的Lock Record
,这里不需要用CAS指令,因为是在safepoint
。 执行完后,就升级成了轻量级锁。原偏向线程的所有Lock Record都已经变成轻量级锁的状态。这里如果看不明白,请回顾上篇文章的轻量级锁加锁过程。偏向锁的释放
偏向锁的释放入口在bytecodeInterpreter.cpp#1923
上面的代码结合注释理解起来应该不难,偏向锁的释放很简单,只要将对应
Lock Record
释放就好了,而轻量级锁则需要将Displaced Mark Word
替换到对象头的mark word中。如果CAS失败或者是重量级锁则进入到InterpreterRuntime::monitorexit
方法中。该方法会在轻量级与重量级锁的文章中讲解。批量重偏向和批量撤销
批量重偏向和批量撤销的背景可以看上篇文章,相关实现在
BiasedLocking::revoke_and_rebias
中:在每次撤销偏向锁的时候都通过
update_heuristics
方法记录下来,以类为单位,当某个类的对象撤销偏向次数达到一定阈值的时候JVM就认为该类不适合偏向模式或者需要重新偏向另一个对象,update_heuristics
就会返回HR_BULK_REVOKE
或HR_BULK_REBIAS
。进行批量撤销或批量重偏向。先看
update_heuristics
方法。当达到阈值的时候就会通过VM 线程在
safepoint
调用bulk_revoke_or_rebias_at_safepoint
, 参数bulk_rebias
如果是true代表是批量重偏向否则为批量撤销。attempt_rebias_of_object
代表对操作的锁对象o
是否运行重偏向,这里是true
。该方法分为两个逻辑:批量重偏向和批量撤销。
先看批量重偏向,分为两步:
code 1
将类中的撤销计数器自增1,之后当该类已存在的实例获得锁时,就会尝试重偏向,相关逻辑在偏向锁获取流程
小节中。code 2
处理当前正在被使用的锁对象,通过遍历所有存活线程的栈,找到所有正在使用的偏向锁对象,然后更新它们的epoch值。也就是说不会重偏向正在使用的锁,否则会破坏锁的线程安全性。批量撤销逻辑如下:
code 3
将类的偏向标记关闭,之后当该类已存在的实例获得锁时,就会升级为轻量级锁;该类新分配的对象的mark word
则是无锁模式。code 4
处理当前正在被使用的锁对象,通过遍历所有存活线程的栈,找到所有正在使用的偏向锁对象,然后撤销偏向锁。The text was updated successfully, but these errors were encountered: