-
Notifications
You must be signed in to change notification settings - Fork 285
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底层实现--重量级锁 #15
Comments
轻量级锁只要有竞争后就膨胀为重量级锁了嘛?好像没有先自旋一定次数失败再膨胀的逻辑,反而是膨胀后再自旋尝试。如果是这样的话,那么假如线程A获取了偏向锁,线程B再访问,然后偏向锁膨胀为轻量级锁,那线程B还是会继续尝试锁,那不就又直接膨胀为重量级锁了嘛,此时的轻量级锁的意义不就没了- - |
B获得轻量级锁后,如没有其他线程获取锁就一直是轻量级锁 |
如果是A线程的偏向锁升级到了轻量级锁,A获取的轻量级锁还未解锁状态下,线程B继续尝试获取锁一次就会升级到重量级锁? |
自旋获取轻量级锁的代码是在哪,我怎么没找到。。 |
您好 看了您的文章受益匪浅 这里有一个小问题 重量级锁级锁时三个队列都没有线程,markword是不是会被替换为无锁状态001,我代码测试的结果是会替换为001,然后再次加锁会从轻量级锁开始,这属于锁降级吗? |
线程 T2 是轻量锁并且正在执行, T3 执行会尝试获取锁, 那么是由 T3 升级锁, 还是 T2 来升级锁呢? |
系列看完了,确实是良心作品!学习了! |
本人拙见,设置为0主要是为了避免如下两个场景发生:
设置为0可以实现,当锁正在膨胀为重量级锁时,延迟拥有锁的线程释放锁。
另一方面,设置为0也避免了获取对象的hash code值与锁膨胀的竞争问题
|
T3来升级,会将锁的_owner设置为T2,这样就不会影响T2使用锁 |
看完重量级锁的膨胀过程后,一直有个问题没想明白: 希望博主和一起学习的同志能指导一下 |
个人拙见:线程在释放重入的2,3次的锁时,只是简单的将lock record中的obj reference置为null,并不关心锁对象的状态,只有当最外层锁(也就是lock record中displace mark word记录着锁对象的mark word)释放时,会回写mark word到锁对象,这个时候失败了才会进入到膨胀环节.此时只有第一次的锁需要释放,所以在膨胀的时候重入计数并不需要处理. |
感谢大佬解答! |
问一下博主,JDK1.6之前的重量级锁实现,为什么是重量的呢?因为现在的重量级锁在加锁的时候不会无脑进行系统调用,而是会通过乐观锁判断时候存在竞争,有竞争才会加锁,其原理基本和reentrantlock一样了,何来“重量一说”?难道是1.6以后重量级锁的逻辑也改了? |
应该来说重量级锁就是利用了阻塞队列来实现了独占锁 |
博主你好,我发现这个地方在前后两篇文章中描述冲突了。 |
默认策略下,在A释放锁后一定是C线程先获得锁。因为在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认策略是:如果EntryList为空,则将cxq中的元素按原有顺序插入到到EntryList,并唤醒第一个线程。也就是当EntryList为空时,是后来的线程先获取锁。这点JDK中的Lock机制是不一样的。 这个是导致synchronized是非公平锁的原因吗? |
@M0rnar |
轻量级应该是没有自旋,只有一次CAS的机会,如果失败了就会升级。在重量级阶段里,有多处自旋获取重量级锁的逻辑tryLock方法就是。我感觉自旋获取轻量级锁是被其他文章误导了,在看源码之前我也一直认为自旋是获取轻量级锁来着 |
看源码如下 将该线程的ObjectWaiter的next指向cxq的头节点(猜的,不懂cpp),所以应该是插入到cxq的队首 |
您好,我想请问一下,偏向锁的CAS和轻量级锁的CAS有没有什么不同,V、A、B分别都是什么值。 |
确实这里好像是这样,只是不知道具体流程是什么。 假设虚拟机启动一段时间后偏向锁模式已开启,线程A最初进入synchronized时完全没有竞争,这时候是偏向锁,但线程A这个同步代码块执行了很长很长的时间。 |
我打印了T1此时的锁状态,不明白为什么是重量级锁,既然线程A一直占用着锁,按照偏向锁撤销那一套逻辑,为什么不是轻量级锁呢?不理解output2为什么不是轻量级锁??
|
A未解,但B已经膨胀,所以此时锁状态是怎样子的呢?轻量级锁还是重量级锁呢? |
此时锁已经升级成重量级了,A会变成锁的持有者,B在尝试获取几次后,会被加入到队列中(EntryList还是cxq记不太清了),等待A释放后B会继续尝试获取 |
搁浅现象是啥,谁能科普一下 |
看了很多文章,关于偏向锁、轻量级锁和重量级锁,偏向锁基于该锁持有者长时间为单一线程,而轻量级锁则是多个线程交替使用,重量级则发生争夺,但说锁升级的时候都是偏向锁->轻量级,那如果再持有轻量级锁的过程中,另一个线程来争抢锁,这个锁是直接升级为重量级锁还是先升级为轻量级锁 |
图片连接失效了 |
搁浅(stranding)现象: monitor 已解锁,但所有争用线程仍处于休眠状态。此时如果没有新的线程来获取锁,已解锁的 monitor 永远无法被正在休眠的线程获取。为了避免这种问题,会选择一个线程作为负责线程(responsible thread),该线程通过调用 time park() 方法,让自己休眠固定时间后被唤醒,随后该线程会检查潜在搁浅问题并从中恢复 |
锁膨胀过程中。若当前是轻量级锁。创建ObjectMonitor对象后,为什么可以简单地将其_recursions设置为0?如果是锁膨胀时,轻量级锁存在重入的情况怎么办? |
轻量级锁升级为重量级锁,初始状态当然得为 0,因为没有发生重入。膨胀过程中,持有锁的线程重入时,会检测到锁在膨胀过程,不能走原有的轻量级锁重入加锁逻辑,会和其他线程一样其他线程一样进行等待(以自旋方式等待),直到膨胀完成,膨胀完成后,持有锁的线程走的就是正常的重量级锁重入逻辑了。 |
为什么不会存在锁重入的情况呢?分析以下情况:
|
轻量级锁释放时, 会判断 Lock Record 记录的 mark word 值是否为 0(即 NULL)
轻量级锁在重入多次后,发生了锁膨胀,此时当轻量级锁释放时,和正常的轻量级锁释放时流程一样,只不过在 Lock Record 记录的 mark word 为 0 的情况下会增加如下诊断:
对于轻量级锁膨胀后,之前的重入释放操作和之前一样,不会走 ObjectMonitor exit 操作,因此在轻量级锁多次重入,然后被膨胀时,ObjectMonitor 的重入计数没必要记录之前的重入次数,使用初始值 0 即可。 |
明白了
|
本文为死磕Synchronized底层实现第三篇文章,内容为重量级锁实现。
本系列文章将对HotSpot的
synchronized
锁实现进行全面分析,内容包括偏向锁、轻量级锁、重量级锁的加锁、解锁、锁升级流程的原理及源码分析,希望给在研究synchronized
路上的同学一些帮助。主要包括以下几篇文章:死磕Synchronized底层实现--概论
死磕Synchronized底层实现--偏向锁
死磕Synchronized底层实现--轻量级锁
死磕Synchronized底层实现--重量级锁
更多文章见个人博客:https://github.com/farmerjohngit/myblog
重量级的膨胀和加锁流程
当出现多个线程同时竞争锁时,会进入到
synchronizer.cpp#slow_enter
方法在
inflate
中完成膨胀过程。inflate
中是一个for循环,主要是为了处理多线程同时调用inflate的情况。然后会根据锁对象的状态进行不同的处理:1.已经是重量级状态,说明膨胀已经完成,直接返回
2.如果是轻量级锁则需要进行膨胀操作
3.如果是膨胀中状态,则进行忙等待
4.如果是无锁状态则需要进行膨胀操作
其中轻量级锁和无锁状态需要进行膨胀操作,轻量级锁膨胀流程如下:
1.调用
omAlloc
分配一个ObjectMonitor
对象(以下简称monitor),在omAlloc
方法中会先从线程私有的monitor
集合omFreeList
中分配对象,如果omFreeList
中已经没有monitor
对象,则从JVM全局的gFreeList
中分配一批monitor
到omFreeList
中。2.初始化
monitor
对象3.将状态设置为膨胀中(INFLATING)状态
4.设置
monitor
的header字段为displaced mark word
,owner字段为Lock Record
,obj字段为锁对象5.设置锁对象头的
mark word
为重量级锁状态,指向第一步分配的monitor
对象无锁状态下的膨胀流程如下:
1.调用
omAlloc
分配一个ObjectMonitor
对象(以下简称monitor)2.初始化
monitor
对象3.设置
monitor
的header字段为mark word
,owner字段为null
,obj字段为锁对象4.设置锁对象头的
mark word
为重量级锁状态,指向第一步分配的monitor
对象至于为什么轻量级锁需要一个膨胀中(INFLATING)状态,代码中的注释是:
我没太看懂,有知道的同学可以指点下~
膨胀完成之后,会调用
enter
方法获得锁EnterI
方法获得锁或阻塞EnterI
方法比较长,在看之前,我们先阐述下其大致原理:一个
ObjectMonitor
对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。
当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个
ObjectWaiter
对象插入到cxq的队列的队首,然后调用park
函数挂起当前线程。在linux系统上,park
函数底层调用的是gclib库的pthread_cond_wait
,JDK的ReentrantLock
底层也是用该方法挂起线程的。更多细节可以看我之前的两篇文章:关于同步的一点思考-下,linux内核级同步机制--futex当线程释放锁时,会从cxq或EntryList中挑选一个线程唤醒,被选中的线程叫做
Heir presumptive
即假定继承人(应该是这样翻译),就是图中的Ready Thread
,假定继承人被唤醒后会尝试获得锁,但synchronized
是非公平的,所以假定继承人不一定能获得锁(这也是它叫"假定"继承人的原因)。如果线程获得锁后调用
Object#wait
方法,则会将线程加入到WaitSet中,当被Object#notify
唤醒后,会将线程从WaitSet移动到cxq或EntryList中去。需要注意的是,当调用一个锁对象的wait
或notify
方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。synchronized
的monitor
锁机制和JDK的ReentrantLock
与Condition
是很相似的,ReentrantLock
也有一个存放等待获取锁线程的链表,Condition
也有一个类似WaitSet
的集合用来存放调用了await
的线程。如果你之前对ReentrantLock
有深入了解,那理解起monitor
应该是很简单。回到代码上,开始分析
EnterI
方法:主要步骤有3步:
这里需要特别说明的是
_Responsible
和_succ
两个字段的作用:当竞争发生时,选取一个线程作为
_Responsible
,_Responsible
线程调用的是有时间限制的park
方法,其目的是防止出现搁浅
现象。_succ
线程是在线程释放锁是被设置,其含义是Heir presumptive
,也就是我们上面说的假定继承人。重量级锁的释放
重量级锁释放的代码在
ObjectMonitor::exit
:在进行必要的锁重入判断以及自旋优化后,进入到主要逻辑:
code 1
设置owner为null,即释放锁,这个时刻其他的线程能获取到锁。这里是一个非公平锁的优化;code 2
如果当前没有等待的线程则直接返回就好了,因为不需要唤醒其他线程。或者如果说succ不为null,代表当前已经有个"醒着的"继承人线程,那当前线程不需要唤醒任何线程;code 3
当前线程重新获得锁,因为之后要操作cxq和EntryList队列以及唤醒线程;code 4
根据QMode的不同,会执行不同的唤醒策略;根据QMode的不同,有不同的处理方式:
只有QMode=2的时候会提前返回,等于0、3、4的时候都会继续往下执行:
1.如果EntryList的首元素非空,就取出来调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回;
2.如果EntryList的首元素为空,就将cxq的所有元素放入到EntryList中,然后再从EntryList中取出来队首元素执行ExitEpilog方法,然后立即返回;
以上对QMode的归纳参考了这篇文章。另外说下,关于如何编译JVM,可以看看该博主的这篇文章,该博主弄了一个docker镜像,傻瓜编译~
QMode默认为0,结合上面的流程我们可以看这么个demo:
默认策略下,在A释放锁后一定是C线程先获得锁。因为在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认策略是:如果EntryList为空,则将cxq中的元素按原有顺序插入到到EntryList,并唤醒第一个线程。也就是当EntryList为空时,是后来的线程先获取锁。这点JDK中的Lock机制是不一样的。
Synchronized和ReentrantLock的区别
原理弄清楚了,顺便总结了几点Synchronized和ReentrantLock的区别:
ReentrantLock#isLocked
判断;ReentrantLock#lockInterruptibly
方法是可以被中断的;End
总的来说Synchronized的重量级锁和ReentrantLock的实现上还是有很多相似的,包括其数据结构、挂起线程方式等等。在日常使用中,如无特殊要求用Synchronized就够了。你深入了解这两者其中一个的实现,了解另外一个或其他锁机制都比较容易,这也是我们常说的技术上的相通性。
The text was updated successfully, but these errors were encountered: