-
Notifications
You must be signed in to change notification settings - Fork 282
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底层实现--轻量级锁 #14
Comments
revoke_bias方法代码中有一行注解 |
@DanFL 已修改 感谢指正 |
我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址, |
我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗? |
另外,在轻量级锁加锁中,CAS的原值若不是从对象中取,而是通过调用对象的hashcode()方法获取,那如果对象所属类的hashcode方法被重写了,获取锁的线程在代码块里头更改锁对象的某个属性值了,岂不是,当线程释放完锁之后,锁对象头里的hashcode变成了一个过期的脏数据hashcode值,那以后所有的线程都没法对这个对象加锁了? |
没看懂你在说什么。。你说的是轻量级加锁的cas过程? 原值是无锁的markword,期望值是当前的Lock Record。所以不会有两个线程同时执行同步块 |
大致明白了你的意思,你可以看下博主上面的源码,每次CAS的话,走的是Atomic::cmpxchg_ptr这个方法,对于compareValue的话,是构建一个无锁状态的Displaced Mark Word,所以线程A获得轻量级锁,修改了markword,但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的,所以无法cas成功 |
我理解应该和内存可见性相关,线程A,B几乎在同时得到monitor enter指令,那么此时二者在各自线程中都存有对象O的缓存,此时必然会导致如果线程A的CAS成功,B的CAS失败 |
你还是成功绕开了我的问题,虽然你回答了不少。你回答:但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的 我的疑问一直都是:怎么会不一样? 传入的无锁状态值从什么地方取来的?难道不是从对象头中取出来?线程A已经获取了锁,现在对象头存储的是线程A的LR,线程B从对象头中取出的是线程A的LR,做CAS替换时,怎么和对象头中存储的不一样?什么叫无锁状态值?我们现在讨论的就是这个东西,你来了个糊弄的回答
在 2019-06-12 16:22:48,"scn7th" <notifications@github.com> 写道:
我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?
大致明白了你的意思,你可以看下博主上面的源码,每次CAS的话,走的是Atomic::cmpxchg_ptr这个方法,对于compareValue的话,是构建一个无锁状态的Displaced Mark Word,所以线程A获得轻量级锁,修改了markword,但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的,所以无法cas成功
我理解应该和内存可见性相关,线程A,B几乎在同时得到monitor enter指令,那么此时二者在各自线程中都存有对象O的缓存,此时必然会导致如果线程A的CAS成功,B的CAS失败
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or mute the thread.
|
我也有这个疑问。 |
我理解线程B中存放的是无锁状态时Mark word的拷贝,并不是引用,即使A已经获得锁,修改了Mark word的值,但是线程B中的Mark word拷贝中的值仍然是hashcode,在做CAS时,取出的线程栈桢里的拷贝值(hashcode),与对象的Mark word中的值(此时是线程A的Lock record地址)比较,必然是不一致的。 |
您好,我理解的轻量级锁加锁过程是: |
如果当前是偏向模式且偏向的线程还在使用锁,那会将锁的mark word改为轻量级锁的状态 我不明白, 偏向线程正在使用锁, 我认为偏向线程还在执行, 另一个线程想要获取锁, 为什么会变成 轻量锁? |
你觉得他会直接变成重量级锁,而不会经过尝试变成轻量级锁失败后再变成重量级锁吗?偏向线程正在执行的时候,另一个线程想获取锁,发现对象头中存了不是自己的其他线程,线程二怎么会知道线程一还在运行中?他当然是尝试变成轻量级锁,变轻量级锁失败后他才知道此时存在竞争,存在竞争说明线程一还在运行中要不然哪来的竞争,这个时候才会升级重量级锁
在 2019-12-30 21:32:54,"sc-ik" <notifications@github.com> 写道:
如果当前是偏向模式且偏向的线程还在使用锁,那会将锁的mark word改为轻量级锁的状态
我不明白, 偏向线程正在使用锁, 我认为偏向线程还在执行, 另一个线程想要获取锁, 为什么会变成 轻量锁?
应该变成重量锁把?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
两者不一样的点就在于最后一个bit的不同。线程B通过lockee->mark()->set_unlocked()拿到的markOop,是 ” 线程A设置的markOop | unlocked_value “,也就是线程B获取到的markOop的最后一位是1(无锁状态),而线程A设置的markOop的最后一位是0(轻量级锁状态)。 |
你第一个问题是因为原值是markword被setunlock之后的(看源码),而不是直接拿的,所以最后的锁标志位会不同,第二个问题对象头的hashcode(此处学名是idendityhashcode)是不会改变的,重写hashcode方法只是对某些类例如HashMap这种显式调用有用,但是你任何时候都可以通过System.identityHashCode()获取到真正的hashcode。 |
请问为什么释放锁的时候也需要cas操作呢?为什么会有失败的情况,只有占了锁的线程才会释放啊 |
因为可能涉及到一个锁膨胀的过程,膨胀时要涉及到创建monitor对象,创建过程中要用到持有轻量级锁的displaced mark wrod 所以要在膨胀的过程中会设置一个INFLATING的状态标志位在对象头中(具体代码在膨胀的文中)来保证能用到 displaced mark wrod,因此CAS操作就是为这个准备的。至于为什么这样做我猜就是为了保证hashcode的一致性吧 |
咨询个关于自旋的问题,网上好多文章关于轻量级锁都说有个自旋的过程,当自旋尝试失败后才膨胀为重量级锁,但是从源码的角度来看,只要CAS失败就会引起锁膨胀;这块能帮忙解释一下吗? |
没有自旋这回事,只有重量级锁获取失败才会自旋,网上的文章好多都是错的,我个人认为轻量级锁的意义就是在没有线程争用锁时不用创建monitor |
没有自旋这回事,也就是说只要有竞争就升级为重量级锁,那和偏向锁没啥区别呀。自旋效率应该更高吧。 |
是的只要存在竞争就会升级重量级。轻量级锁的存在就是用于线程之间交替获取锁的场景,但是和偏向锁是有区别的啊。一个线程获取偏向锁之后,那么这个锁自然而然就属于这个线程(就算该线程释放了偏向锁也不会改变这把锁偏向这个线程的,这个前提是没有发生过批量重偏向使锁的epoch与其对应class类的epoch不相等)。所以说偏向锁的场景是用于一个线程不断的获取锁,如果把它放在轻量级锁的场景下线程之间交替获取的话会发生偏向锁的撤销的。 |
好的,谢谢,看了一篇文章说轻量级锁有自旋,还说的挺有道理的,哈哈。。。。。还是源码出真理啊。 |
对的网上很多都是这样说的,我以前也是这样认为的。估计是被并发编程艺术这本书误导了,把重量级中的锁的自旋看成了轻量级也有 |
@flying81621 不知道你还有疑惑吗
首先A线程通过CAS替换markword替换成功,表明A线程获取了锁,这里应该大家都没问题。 |
1 similar comment
@flying81621 不知道你还有疑惑吗
首先A线程通过CAS替换markword替换成功,表明A线程获取了锁,这里应该大家都没问题。 |
为什么轻量级解锁过程要处理线程栈中所有的lock record?假设有两个重入的lock record和一个非重入的lock record,那么第一次解锁过程不应该是释放一个非重入的lock record就行了吗? |
@farmerjohngit 话说博主不维护这个博客了吗?好久都没看到回复了 |
// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.
// Because some machines have alignment restrictions on the control stack,
// the actual space allocated by the interpreter may include padding words
// after the end of the BasicObjectLock. Also, in order to guarantee
// alignment of the embedded BasicLock objects on such machines, we
// put the embedded BasicLock at the beginning of the struct.
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
BasicLock _lock; // the lock, must be double word aligned
oop _obj; // object holds the lock; 仔细看了一下 BasicObjectLock 的注释,确实因为这个原因 |
取内存上的值,然后cas,这是两步操作,非原子的。 A线程取到内存addr上的值 a,此时A线程尚未进行cas,B 线程把addr上的值改为b, 这时A线程再CAS发现原值不是a就会失败,这不是cas的基本概念吗,你在想什么。 |
有个问题,纯小白,轻量级锁entry->lock()->set_displaced_header(displaced);,这个方法,entry是一个BasicObjectLock,对象,entry.lock返回的是一个BasicLock对象,BasicLock里面_displaced_header才是存的他的对象头的信息,但是看你的文章,一个lock record对象里面就有个markword和obj对象,有点迷。 |
我在另外一篇看到了,basiclock和basicobjectlock是lockrecord的实现,现在的新问题是,java虚拟机为什么要这么搞?openjdk里面一个lockrecord没存什么东西,除了存markoop还存了个oop指向markword,这样是不是有点多此一举了? |
1.除了从偏向锁升级,轻量级锁获取的前提是无锁状态,都已经是轻量级锁还走什么偏向流程,看看monitorenter和slowenter进入CAS代码的前提是什么 2.就算真进了偏向流程,markOop displaced = lockee->mark()->set_unlocked()这句没看到么 |
在撤销偏向的时候。在发现持有偏向锁的线程存活并且还在同步块中的时候。查看源码,会走下面这段核心逻辑完成锁升级的第一步, 源码位置 。 if (highest_lock != NULL) {
//CODE1: 这里的unbiased_prototype是将无锁状态暂存到lockrecord中,为后续解锁恢复成无锁使用
highest_lock->set_displaced_header(unbiased_prototype);
//CODE2:
obj->set_mark(markOopDesc::encode(highest_lock));
} 我的疑问: 上面CODE2之前锁状态应是偏向锁101,可CODE2看代码只是修改对象头Mark Word 中非锁状态位的值,即只修改recored指针位的值(C++代码我不太确定),但没有修改锁状态位吧? 本想做JVM的调试,可是没搞定,所以不确定这个之后Mark Word 中锁状态位是101还是001,或者其他? 我一直在找代码线索,想确定我说的这个场景(在偏向锁状态,并且偏向线程还在同步块中),当有竞争线程进入时,怎么从101变成010重量级锁的。 这里锁对象实例的锁状态过程是:101 -> 001-> 010? 还是 101->010 ? 还是其他? 因为上面这段代码感觉是我破解线索的第一步,可是刚好这步就无法确定。哎!不熟悉C++的硬伤。 求大家解惑! |
你好,请问这个对齐原因和锁状态被设置为00有什么关系呢? |
如果根本没有轻量级锁的自旋这回事的话, |
代码里没搜到,我看的jdk8u的代码 |
有一个疑问,在偏向锁撤销中,将所有与锁对象相关的 Lock Record 中的 if (highest_lock != NULL) {
// 修改第一个Lock Record为无锁状态,然后将obj的mark word设置为指向该Lock Record的指针
highest_lock->set_displaced_header(unbiased_prototype);
obj->release_set_mark(markOopDesc::encode(highest_lock));
...
} 在轻量级锁获取的过程中,也是构建了一个无锁状态的 // traditional lightweight locking
if (!success) {
// 构建一个无锁状态的Displaced Mark Word
markOop displaced = lockee->mark()->set_unlocked();
// 设置到Lock Record中去
entry->lock()->set_displaced_header(displaced);
bool call_vm = UseHeavyMonitors;
if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
// 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
// Is it simple recursive case?
if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
entry->lock()->set_displaced_header(NULL);
} else {
// CAS操作失败则调用monitorenter
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
}
} |
锁对象还原成无锁状态之后说明不能再偏向了(匿名偏向或者已经偏向的情况下才能继续申请偏向锁),下次对这个锁对象加锁得先从轻量级锁加起 |
这个对齐为什么就能说明它最后两位是00了呀?求解答!!! |
我悟了。因为双字对齐,也就是4字节对齐,一个地址4字节对齐必须能被4整除。所以最后两位都是0,才能4字节对齐。 |
您的意思是说,_lock的地址必须是4的倍数?这是如何实现的?对齐如何理解啊? |
是的。_lock得是4字节对齐的。具体的可以搜索c语言4字节对齐,是和cpu内存有关的概念。 |
是的。4字节对齐要求,起始地址是4字节对齐,整个结构体长度要4字节对齐。这个4字节对齐是为了方便cpu读取内存数据,具体的我也不知道。 |
您好,我还想请教您一个问题,就是批量重偏向有什么作用?我看在偏向锁的处理流程中,对于批量重偏向,是尝试使用CAS进行重偏向操作, 失败则进行锁撤销。如果没有开启批量重偏向,那么后续的操作也是使用CAS进行重偏向操作,失败就会进行撤销。这完全没有体现出批量重偏向的作用啊? |
兄弟 有自旋的!!锁的膨胀过程通过ObjectSynchronizer::inflate函数,在这里面有自旋膨胀,在膨胀完成之后会执行enter方法,在enter中会调用TrySpin_VaryDuration 这个里面进行自旋了,并且这里的自旋是在锁膨胀为重量级锁之后自旋的,目的是为了尽可能的少进入内核态。。 |
要满足你说的,在偏向锁结束之后,锁的标志位就必须得归为无锁状态才行,这样才能互斥。如果一个锁升级为轻量级锁,在退出之后不会退出为无锁状态,那么下一个线程获取到的锁终究是轻量级锁,按照你的说法,不是就直接膨胀了,这样轻量级锁就失去了它的意义。如果锁在退出的时候归还为无锁状态,存在锁降级的可能性,是否又需要从偏向模式开始一步一步的升级锁? |
按照分析,那下面这种情况,为什么会升级为轻量级锁啊 public static void main(String[] args) throws InterruptedException {
sleep(5000);
Object object = new Object();
new Thread(() -> {
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}, "t1").start();
sleep(5000);
new Thread(() -> {
synchronized (object) {
//这里升级为轻量级锁,不该是偏向么?
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}, "t2").start();
} 另外,如果没有自旋的话,那线程A是偏向锁,线程B来了,在安全点修改为轻量后,B再CAS竞争轻量锁,失败,就直接升级为重量锁了么 |
A和B争抢 |
我觉得啊 源码区分了这个到底是LR还是markword |
|
请教一下,在解锁是为什么会存在CAS失败这种情况呢? |
谢谢你回答了关于锁膨胀为重量级锁之后自旋,但是说人家说的是轻量锁是没有自旋 |
你可能没有明白什么叫做轻量级锁、什么叫做重量级锁 |
6 |
最近研究synchronized源码,发现这篇文章其实将的很好了. 然后发现有很多人在纠结这个 if (mark->has_bias_pattern()) {
if(xxxx)
else if (xxxxx)
else {
markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |
(uintptr_t)markOopDesc::age_mask_in_place |
epoch_mask_in_place));
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
markOop new_header = (markOop) ((uintptr_t) header | thread_ident);
if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
(* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
}
else {
// IMPORTANT
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
} 上面讨论的两个线程A,B 一起来拿锁, 然后A线程获取到锁了, B线程走进 总结一下: |
|
死磕Synchronized底层实现--轻量级锁
本文为死磕Synchronized底层实现第三篇文章,内容为轻量级锁实现。
轻量级锁并不复杂,其中很多内容在偏向锁一文中已提及过,与本文内容会有部分重叠。
另外轻量级锁的背景和基本流程在概论中已有讲解。强烈建议在看过两篇文章的基础下阅读本文。
本系列文章将对HotSpot的
synchronized
锁实现进行全面分析,内容包括偏向锁、轻量级锁、重量级锁的加锁、解锁、锁升级流程的原理及源码分析,希望给在研究synchronized
路上的同学一些帮助。主要包括以下几篇文章:死磕Synchronized底层实现--概论
死磕Synchronized底层实现--偏向锁
死磕Synchronized底层实现--轻量级锁
死磕Synchronized底层实现--重量级锁
更多文章见个人博客:https://github.com/farmerjohngit/myblog
本文分为两个部分:
1.轻量级锁获取流程
2.轻量级锁释放流程
本人看的JVM版本是jdk8u,具体版本号以及代码可以在这里看到。
轻量级锁获取流程
下面开始轻量级锁获取流程分析,代码在bytecodeInterpreter.cpp#1816。
如果锁对象不是偏向模式或已经偏向其他线程,则
success
为false
。这时候会构建一个无锁状态的mark word
设置到Lock Record
中去,我们称Lock Record
中存储对象mark word
的字段叫Displaced Mark Word
。如果当前锁的状态不是无锁状态,则CAS失败。如果这是一次锁重入,那直接将
Lock Record
的Displaced Mark Word
设置为null
。我们看个demo,在该demo中重复3次获得锁,
假设锁的状态是轻量级锁,下图反应了
mark word
和线程栈中Lock Record
的状态,可以看到右边线程栈中包含3个指向当前锁对象的Lock Record
。其中栈中最高位的Lock Record
为第一次获取锁时分配的。其Displaced Mark word
的值为锁对象的加锁前的mark word
,之后的锁重入会在线程栈中分配一个Displaced Mark word
为null
的Lock Record
。为什么JVM选择在线程栈中添加
Displaced Mark word
为null的Lock Record
来表示重入计数呢?首先锁重入次数是一定要记录下来的,因为每次解锁都需要对应一次加锁,解锁次数等于加锁次数时,该锁才真正的被释放,也就是在解锁时需要用到说锁重入次数的。一个简单的方案是将锁重入次数记录在对象头的mark word
中,但mark word
的大小是有限的,已经存放不下该信息了。另一个方案是只创建一个Lock Record
并在其中记录重入次数,Hotspot没有这样做的原因我猜是考虑到效率有影响:每次重入获得锁都需要遍历该线程的栈找到对应的Lock Record
,然后修改它的值。所以最终Hotspot选择每次获得锁都添加一个
Lock Record
来表示锁的重入。接下来看看
InterpreterRuntime::monitorenter
方法fast_enter
的流程在偏向锁一文已经分析过,如果当前是偏向模式且偏向的线程还在使用锁,那会将锁的mark word
改为轻量级锁的状态,同时会将偏向的线程栈中的Lock Record
修改为轻量级锁对应的形式。代码位置在biasedLocking.cpp#212。我们看
slow_enter
的流程。轻量级锁释放流程
轻量级锁释放时需要将
Displaced Mark Word
替换到对象头的mark word
中。如果CAS失败或者是重量级锁则进入到InterpreterRuntime::monitorexit
方法中。monitorexit
调用完slow_exit
方法后,就释放Lock Record
。该方法中先判断是不是轻量级锁,如果是轻量级锁则将替换
mark word
,否则膨胀为重量级锁并调用exit
方法,相关逻辑将在重量级锁的文章中讲解。The text was updated successfully, but these errors were encountered: