Open farmerjohngit opened 6 years ago
我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说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什么场景下会失败,能举出具体案例吗?
@flying81621 我觉得您想问的应该是下面这个 CAS 操作是怎么实现轻量级锁的互斥的。
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); } } }
假设现在有两个线程,线程 A 首先通过锁撤销的一系列逻辑最终通过
slow_enter
方法中如下的代码把线程 A 的 Lock Record 中的 displaced header 的在线程 A 的线程栈中的地址通过 CAS 操作写进了锁对象的 mark word 中,即线程 A 成功获取了锁,继续执行同步代码块。if (mark->is_neutral()) { lock->set_displaced_header(mark); // 请注意 CAS 操作的修改值 lock,lock 为指针类型 if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } }
我再重申一遍这个线程 A 获取轻量级锁的 CAS 操作的逻辑,把一个四字节的指针写进了锁对象的 mark word 中,所以后面取锁对象的 mark word 时会取到一个指针。 而当线程 B 执行到同步代码块准备尝试获取轻量级锁时在 CAS 操作时里用的“预期原值”为
markOop displaced = lockee->mark()->set_unlocked()
,而这个构造出来的 displaced mark word 为线程 A 的线程栈里 Lock Record 中 displaced mark word 的地址“位或”上1
(具体可以看下set_unlocked
方法的实现)使得 displaced 的最低位为1
,所以要想这个 CAS 操作失败的话(因为只有操作失败才能满足锁的互斥性),那必然lockee->mark()
取到地址最低位应该为0
才行。 下面纯属个人推测: 这里应该是用了某种字节对齐方式导致线程 A 中线程栈里 Lock Record 中 displaced mark word 地址的最低两位为00
,回想一下概论里的 mark word 状态转换图,在轻量级锁下最低两位为00
,而之前的 CAS 操作只写了一个地址(四字节,mark word 也是四字节),并没有专门的去改最低两位的值为00
(如果有请通知我一下,谢谢🙏,我个人找了半天没找到)。所以基于以上的方式才能实现轻量级锁的互斥。 再次重申,个人推测,错了不要骂我(要是R大来回答一下这问题就好了,唉)。// 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 的注释,确实因为这个原因
你好,请问这个对齐原因和锁状态被设置为00有什么关系呢?
我悟了。因为双字对齐,也就是4字节对齐,一个地址4字节对齐必须能被4整除。所以最后两位都是0,才能4字节对齐。
您的意思是说,_lock的地址必须是4的倍数?这是如何实现的?对齐如何理解啊?
是的。4字节对齐要求,起始地址是4字节对齐,整个结构体长度要4字节对齐。这个4字节对齐是为了方便cpu读取内存数据,具体的我也不知道。
您好,我还想请教您一个问题,就是批量重偏向有什么作用?我看在偏向锁的处理流程中,对于批量重偏向,是尝试使用CAS进行重偏向操作, 失败则进行锁撤销。如果没有开启批量重偏向,那么后续的操作也是使用CAS进行重偏向操作,失败就会进行撤销。这完全没有体现出批量重偏向的作用啊?
咨询个关于自旋的问题,网上好多文章关于轻量级锁都说有个自旋的过程,当自旋尝试失败后才膨胀为重量级锁,但是从源码的角度来看,只要CAS失败就会引起锁膨胀;这块能帮忙解释一下吗?
没有自旋这回事,只有重量级锁获取失败才会自旋,网上的文章好多都是错的,我个人认为轻量级锁的意义就是在没有线程争用锁时不用创建monitor
兄弟 有自旋的!!锁的膨胀过程通过ObjectSynchronizer::inflate函数,在这里面有自旋膨胀,在膨胀完成之后会执行enter方法,在enter中会调用TrySpin_VaryDuration 这个里面进行自旋了,并且这里的自旋是在锁膨胀为重量级锁之后自旋的,目的是为了尽可能的少进入内核态。。 给你个参考帖子https://www.cnblogs.com/dennyzhangdd/p/6734638.html https://www.zhihu.com/question/39009953/answer/80186008
@flying81621 不知道你还有疑惑吗
我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说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什么场景下会失败,能举出具体案例吗?
首先A线程通过CAS替换markword替换成功,表明A线程获取了锁,这里应该大家都没问题。 然后是B线程,你的问题取决于B线程是什么时候到达synchronized的。 1.如果它和A几乎同时到达, B就会看到对象头当前处于无锁状态,B就会进行CAS去尝试替换对象的markword,那么A和B之中必定只有一个线程会成功,另一个就是失败的。 2.如果它是在A已经CAS完成后才到达的synchronized,那么B就会看到对象的markword就不是无锁状态,B就会知道当前存在并发,它接下去要做就是锁膨胀流程,这个时候B要把markword改成inflating状态就是全部都是0(同样也是使用CAS),来告诉其他线程正在进行锁膨胀。 不知道我的回答能否解决你心中的疑惑
@flying81621 不知道你还有疑惑吗
我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说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什么场景下会失败,能举出具体案例吗?
首先A线程通过CAS替换markword替换成功,表明A线程获取了锁,这里应该大家都没问题。 然后是B线程,你的问题取决于B线程是什么时候到达synchronized的。 1.如果它和A几乎同时到达, B就会看到对象头当前处于无锁状态,B就会进行CAS去尝试替换对象的markword,那么A和B之中必定只有一个线程会成功,另一个就是失败的。 2.如果它是在A已经CAS完成后才到达的synchronized,那么B就会看到对象的markword就不是无锁状态,B就会知道当前存在并发,它接下去要做就是锁膨胀流程,这个时候B要把markword改成inflating状态就是全部都是0(同样也是使用CAS),来告诉其他线程正在进行锁膨胀。 不知道我的回答能否解决你心中的疑惑
要满足你说的,在偏向锁结束之后,锁的标志位就必须得归为无锁状态才行,这样才能互斥。如果一个锁升级为轻量级锁,在退出之后不会退出为无锁状态,那么下一个线程获取到的锁终究是轻量级锁,按照你的说法,不是就直接膨胀了,这样轻量级锁就失去了它的意义。如果锁在退出的时候归还为无锁状态,存在锁降级的可能性,是否又需要从偏向模式开始一步一步的升级锁?
按照分析,那下面这种情况,为什么会升级为轻量级锁啊
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竞争轻量锁,失败,就直接升级为重量锁了么
你还是成功绕开了我的问题,虽然你回答了不少。你回答:但当线程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.
A和B争抢 A 期望值 01无锁, 写00;假如A成功; 此时B 期望值 01无锁, 写00,但是A已经写00了,所以CAS会失败
我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说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什么场景下会失败,能举出具体案例吗?
我觉得啊 源码区分了这个到底是LR还是markword
我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说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什么场景下会失败,能举出具体案例吗?
我觉得啊 源码区分了这个到底是LR还是markword
你看最后两位是可以确定的
请教一下,在解锁是为什么会存在CAS失败这种情况呢?
咨询个关于自旋的问题,网上好多文章关于轻量级锁都说有个自旋的过程,当自旋尝试失败后才膨胀为重量级锁,但是从源码的角度来看,只要CAS失败就会引起锁膨胀;这块能帮忙解释一下吗?
没有自旋这回事,只有重量级锁获取失败才会自旋,网上的文章好多都是错的,我个人认为轻量级锁的意义就是在没有线程争用锁时不用创建monitor
兄弟 有自旋的!!锁的膨胀过程通过ObjectSynchronizer::inflate函数,在这里面有自旋膨胀,在膨胀完成之后会执行enter方法,在enter中会调用TrySpin_VaryDuration 这个里面进行自旋了,并且这里的自旋是在锁膨胀为重量级锁之后自旋的,目的是为了尽可能的少进入内核态。。 给你个参考帖子https://www.cnblogs.com/dennyzhangdd/p/6734638.html https://www.zhihu.com/question/39009953/answer/80186008
谢谢你回答了关于锁膨胀为重量级锁之后自旋,但是说人家说的是轻量锁是没有自旋
如果当前是偏向模式且偏向的线程还在使用锁,那会将锁的mark word改为轻量级锁的状态
我不明白, 偏向线程正在使用锁, 我认为偏向线程还在执行, 另一个线程想要获取锁, 为什么会变成 轻量锁? 应该变成重量锁把?
你可能没有明白什么叫做轻量级锁、什么叫做重量级锁
6
我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说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什么场景下会失败,能举出具体案例吗?
@flying81621 我觉得您想问的应该是下面这个 CAS 操作是怎么实现轻量级锁的互斥的。
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); } } }
假设现在有两个线程,线程 A 首先通过锁撤销的一系列逻辑最终通过
slow_enter
方法中如下的代码把线程 A 的 Lock Record 中的 displaced header 的在线程 A 的线程栈中的地址通过 CAS 操作写进了锁对象的 mark word 中,即线程 A 成功获取了锁,继续执行同步代码块。if (mark->is_neutral()) { lock->set_displaced_header(mark); // 请注意 CAS 操作的修改值 lock,lock 为指针类型 if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } }
我再重申一遍这个线程 A 获取轻量级锁的 CAS 操作的逻辑,把一个四字节的指针写进了锁对象的 mark word 中,所以后面取锁对象的 mark word 时会取到一个指针。 而当线程 B 执行到同步代码块准备尝试获取轻量级锁时在 CAS 操作时里用的“预期原值”为
markOop displaced = lockee->mark()->set_unlocked()
,而这个构造出来的 displaced mark word 为线程 A 的线程栈里 Lock Record 中 displaced mark word 的地址“位或”上1
(具体可以看下set_unlocked
方法的实现)使得 displaced 的最低位为1
,所以要想这个 CAS 操作失败的话(因为只有操作失败才能满足锁的互斥性),那必然lockee->mark()
取到地址最低位应该为0
才行。 下面纯属个人推测: 这里应该是用了某种字节对齐方式导致线程 A 中线程栈里 Lock Record 中 displaced mark word 地址的最低两位为00
,回想一下概论里的 mark word 状态转换图,在轻量级锁下最低两位为00
,而之前的 CAS 操作只写了一个地址(四字节,mark word 也是四字节),并没有专门的去改最低两位的值为00
(如果有请通知我一下,谢谢🙏,我个人找了半天没找到)。所以基于以上的方式才能实现轻量级锁的互斥。 再次重申,个人推测,错了不要骂我(要是R大来回答一下这问题就好了,唉)。
最近研究synchronized源码,发现这篇文章其实将的很好了. 然后发现有很多人在纠结这个!success
方法的cas的问题. 翻完了所有的留言, 发现其实都是错的.
如果看源码仔细, 并且画过流程图之类的,就会发现, 进入轻量级锁的逻辑会在两个地方
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线程走进!success
的逻辑本身就是有前提的. 就是此时B线程加锁的对象的对象头已经不是01
了, 如果是01
那么加锁逻辑一定是在上面这个if分支里面去进行的. 所以, 进入!success
分支的前提条件都已经被篡改了, 怎么可能会发现问题的本质.
如果想要说B线程获取的对象头是01状态(偏向/无锁)那么走到一定IMPORTANT
表示的锁升级流程(止于为什么只会进入else不会进入其他if 或者else if分支请多读一遍上一章偏向锁批量撤销和重偏向逻辑, 里面会把正在涉及加锁的对象实例的数据做同步, 也就意味着, 但凡一个对象实例还在被线程加锁, 那么就不会和klass有区别. 有区别一定是当前这个对象已经不被线程加锁了, 这里会比较绕,自己多嚼), 如果B线程获取的对象头是00状态或者10状态, 那么经过markOop displaced = lockee->mark()->set_unlocked();
方法之后这个标识位就会改成01或者11 ,那么已经和原始的值不一样了. 怎么可能CAS成功.
至于更离谱的字节对齐的问题就更离谱了. 不可能莫名其妙去修改一个地址吧. 都看源码了, 还在相信玄学
总结一下:
如果B线程这个时候拿的对象头是01标识位, 那么会走if(mark->has_bias_pattern())
的逻辑
如果B线程这个时候拿的对象头是10/0 标识位, 那么在 markOop displaced = lockee->mark()->set_unlocked();
逻辑中就会修改这个标识位,导致CAS失败
我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说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什么场景下会失败,能举出具体案例吗?
@flying81621 我觉得您想问的应该是下面这个 CAS 操作是怎么实现轻量级锁的互斥的。
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); } } }
假设现在有两个线程,线程 A 首先通过锁撤销的一系列逻辑最终通过
slow_enter
方法中如下的代码把线程 A 的 Lock Record 中的 displaced header 的在线程 A 的线程栈中的地址通过 CAS 操作写进了锁对象的 mark word 中,即线程 A 成功获取了锁,继续执行同步代码块。if (mark->is_neutral()) { lock->set_displaced_header(mark); // 请注意 CAS 操作的修改值 lock,lock 为指针类型 if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } }
我再重申一遍这个线程 A 获取轻量级锁的 CAS 操作的逻辑,把一个四字节的指针写进了锁对象的 mark word 中,所以后面取锁对象的 mark word 时会取到一个指针。 而当线程 B 执行到同步代码块准备尝试获取轻量级锁时在 CAS 操作时里用的“预期原值”为
markOop displaced = lockee->mark()->set_unlocked()
,而这个构造出来的 displaced mark word 为线程 A 的线程栈里 Lock Record 中 displaced mark word 的地址“位或”上1
(具体可以看下set_unlocked
方法的实现)使得 displaced 的最低位为1
,所以要想这个 CAS 操作失败的话(因为只有操作失败才能满足锁的互斥性),那必然lockee->mark()
取到地址最低位应该为0
才行。 下面纯属个人推测: 这里应该是用了某种字节对齐方式导致线程 A 中线程栈里 Lock Record 中 displaced mark word 地址的最低两位为00
,回想一下概论里的 mark word 状态转换图,在轻量级锁下最低两位为00
,而之前的 CAS 操作只写了一个地址(四字节,mark word 也是四字节),并没有专门的去改最低两位的值为00
(如果有请通知我一下,谢谢🙏,我个人找了半天没找到)。所以基于以上的方式才能实现轻量级锁的互斥。 再次重申,个人推测,错了不要骂我(要是R大来回答一下这问题就好了,唉)。最近研究synchronized源码,发现这篇文章其实将的很好了. 然后发现有很多人在纠结这个
!success
方法的cas的问题. 翻完了所有的留言, 发现其实都是错的. 如果看源码仔细, 并且画过流程图之类的,就会发现, 进入轻量级锁的逻辑会在两个地方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线程走进
!success
的逻辑本身就是有前提的. 就是此时B线程加锁的对象的对象头已经不是01
了, 如果是01
那么加锁逻辑一定是在上面这个if分支里面去进行的. 所以, 进入!success
分支的前提条件都已经被篡改了, 怎么可能会发现问题的本质. 如果想要说B线程获取的对象头是01状态(偏向/无锁)那么走到一定IMPORTANT
表示的锁升级流程(止于为什么只会进入else不会进入其他if 或者else if分支请多读一遍上一章偏向锁批量撤销和重偏向逻辑, 里面会把正在涉及加锁的对象实例的数据做同步, 也就意味着, 但凡一个对象实例还在被线程加锁, 那么就不会和klass有区别. 有区别一定是当前这个对象已经不被线程加锁了, 这里会比较绕,自己多嚼), 如果B线程获取的对象头是00状态或者10状态, 那么经过markOop displaced = lockee->mark()->set_unlocked();
方法之后这个标识位就会改成01或者11 ,那么已经和原始的值不一样了. 怎么可能CAS成功. 至于更离谱的字节对齐的问题就更离谱了. 不可能莫名其妙去修改一个地址吧. 都看源码了, 还在相信玄学总结一下: 如果B线程这个时候拿的对象头是01标识位, 那么会走
if(mark->has_bias_pattern())
的逻辑 如果B线程这个时候拿的对象头是10/0 标识位, 那么在markOop displaced = lockee->mark()->set_unlocked();
逻辑中就会修改这个标识位,导致CAS失败
001
无锁状态会进入到!success
的逻辑分支,这时候直接CAS成功获取轻量级锁 . 并不会进入到 if (mark->has_bias_pattern())
这个分支,has_bias_pattern源码看这里
死磕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
方法,相关逻辑将在重量级锁的文章中讲解。