Synchronized原理分析
2020-12-29 13:28:34 0 举报
本流程图根据openJDK12源码一步一步详细解析了synchronized的整体加锁以及解锁流程,以及整个锁升级的流程
作者其他创作
大纲/内容
轻量锁膨胀
判断UseBiasedLocking是否开启
判断class的prototype_header或对象的mark word中偏向模式是否关闭(anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0
批量重偏向
false
先自旋尝试获取锁,如果可以则直接获取锁返回TrySpin(Self) > 0通过Spin实现的自旋
轻量锁的加锁逻辑
1.没有开启-XX:+UseHeavyMonitors参数2.CAS操作失败,说明此时锁对象不是无状态,已经有线程占用了此时需要判断是否重入
判断mark word中偏向标志是否为1
1 将类的偏向标记关闭,之后当该类已存在的实例获得锁时,就会升级为轻量级锁;该类新分配的对象的mark word则是无锁模式。 2 处理当前正在被使用的锁对象,通过遍历所有存活线程的栈,找到所有正在使用的偏向锁对象,然后撤销偏向锁。
true
对于这里重入的设计,为什么要创建一个null的Lock Record?对比分析以下几个方案1. 只使用一个Lock Record,在里面记重入的次数,每次重入就需要遍历当前线程引用的Lock Record并修改其中的计数值,效率低,损耗性能。2. 将重入次数记录在对象的对象头中,目前对象头中存储的数据已经饱和了,没有多余的位置再来存储其他信息,对象头大小收到了限制所以JVM对于重入直接创建一个mark work为null的Lock Record指向锁对象来记录重入次数,解锁时只需要一一释放即可。
源码疑问:以下代码是干啥用的if (PrintBiasedLockingStatistics) { (* BiasedLocking::biased_lock_entry_count_addr())++;}个人理解看完轻量锁的逻辑之后,在源码update_heuristics方法中找到了答案,这个应该是记录偏向的次数,达到一定的次数触发批量重偏向或者是批量撤销的
源码位置:openjdk12\\jdk12-06222165c35f\\src\\hotspot\\share\untime\\synchronizer.cpp --- 363行 ObjectSynchronizer::slow_enteropenjdk12\\jdk12-06222165c35f\\src\\hotspot\\share\untime\\synchronizer.cpp --- 1315行 ObjectSynchronizer::inflateopenjdk12\\jdk12-06222165c35f\\src\\hotspot\\share\untime\\objectMonitor.cpp --- 241行 ObjectMonitor::enteropenjdk12\\jdk12-06222165c35f\\src\\hotspot\\share\untime\\objectMonitor.cpp --- 418行 ObjectMonitor::EnterIopenjdk12\\jdk12-06222165c35f\\src\\hotspot\\share\untime\\objectMonitor.cpp --- 584行 ObjectMonitor::exit
锁升级
进入同步代码块for(;;) EnterI(THREAD)
for(;;) 通过CAS将ObjectWaiter插入cxq列表中,失败则会进入TryLock方法中尝试获取锁。降低插入cxq列表中的频次
流程跑到这里说明该锁对象要么是已经偏向别的线程,要么就是匿名偏向(没有偏向任何线程)
CAS失败
JAVA
栈
VM
success = false
源码位置:openjdk12\\jdk12-06222165c35f\\src\\hotspot\\share\\interpreter\\bytecodeInterpreter.cpp --- 1775行 _monitorenter
重量锁是在当出现多个线程同时竞争锁时,会进入到synchronizer.cpp#slow_enter方法1. 这时候需要膨胀为重量级锁,膨胀前,设置Displaced Mark Word为一个特殊值,代表该锁正在用一个重量级锁的monitor2. 先调用inflate膨胀为重量级锁,该方法返回一个ObjectMonitor对象,然后调用其enter方法
ObjectMonitor::enter
重量锁的加锁逻辑
mark word
以下是基于openJDK12源码分析的流程图
revoke_bias方法逻辑:1. 查看偏向的线程是否存活,如果已经不存活了,则直接撤销偏向锁。JVM维护了一个集合存放所有存活的线程,通过遍历该集合判断某个线程是否存活。2. 偏向的线程是否还在同步块中,如果不在了,则撤销偏向锁。我们回顾一下偏向锁的加锁流程:每次进入同步块(即执行monitorenter)的时候都会以从高往低的顺序在栈中找到第一个可用的Lock Record,将其obj字段指向锁对象。每次解锁(即执行monitorexit)的时候都会将最低的一个相关Lock Record移除掉。所以可以通过遍历线程栈中的Lock Record来判断线程是否还在同步块中。3. 将偏向线程所有相关Lock Record的Displaced Mark Word设置为null,然后将最高位的Lock Record的Displaced Mark Word 设置为无锁状态,最高位的Lock Record也就是第一次获得锁时的Lock Record(这里的第一次是指重入获取锁时的第一次),然后将对象头指向最高位的Lock Record,这里不需要用CAS指令,因为是在safepoint。 执行完后,就升级成了轻量级锁。原偏向线程的所有Lock Record都已经变成轻量级锁的状态。这里如果看不明白,请再回顾游戏啊上篇文章的轻量级锁加锁过程。
synchronized关键字的底层实现原理:在字节码层面上是通过_monitorenter和_monitorexit两个指令实现的,_monitorenter位于同步代码块的开始位置,_monitorexit位于同步代码的结束位置,当指令执行到_monitorenter指令时表示该线程已经进入同步代码块,执行到_monitorexit时表示线程即将退出同步代码块,会有两个_monitorenter指令,第一_monitorexit指令是正常释放锁,第二个_monitorexit指令是同步代码块遇到error或者excepion时保证能够正常释放锁 ,对于同步方法来说字节码中相比普通方法会在常量池中多了一个ACC_SYNCHRONIZED访问标志,当线程对该同步方法进行访问时就会设置该标识,获取monitor对象执行方法体,其他线程再次访问时判断该标识位,已经被设置无法获得同一个monitor对象。
获取锁对象,寻找一个空闲的Lock Record,并将Lock Record中的obj ref引用指向该锁对象
偏向锁的加锁逻辑
这边不是我们需要关注的
synchronizer.cpp#inflate通过for(;;)自旋流转到不同的实现逻辑中
success = true
当_Responsible是自己的时候则会调用有时间限制的park,如果不是则直接park
遍历线程栈中所有的Lock Record,找到线程对应的Lock Record,需要升级为轻量级锁,直接修改偏向线程栈中的Lock Record。为了处理锁重入的case,在这里将Lock Record的Displaced Mark Word设置为null,第一个Lock Record会在下面的代码中再处理
通过计算判断锁对象是否已经偏向自己且epoch_mask_in_place是否等于class中的epoch_mask_in_placeanticipated_bias_locking_value == 0
在锁对象的mark work中设置INFLATING状态的理由有以下2点:1. 在膨胀操作过程中有线程来进行膨胀操作,可以根据锁对象的mark word中的INFLATING状态来判断是否有线程正在进行膨胀操作。2。在膨胀过程中有获取到锁对象的线程进行释放锁的操作,可以根据锁对象的mark word中的INFLATING状态来判断是否可以进行释放锁的操作
重量锁的枷锁逻辑
获取锁对象的mark word
创建一个mark word为null的Lock Record,并将object ref指向该锁对象
作者:Kevin日期:2020-12-24
利用CAS指令尝试偏向当前线程,创建一个匿名偏向的mark word,创建一个偏向当前线程的mark word,并将锁对象的mark word替换成偏向当前线程的mark word
object ref
这种场景是性能最高的,只需要做一次计算,之后啥事也不用做
利用CAS指令尝试撤销偏向锁,将锁对象的mark word替换成class的prototype_header的mark word
判断是VM线程还是JAVA线程
1 将类中的撤销计数器自增1,之后当该类已存在的实例获得锁时,就会尝试重偏向,相关逻辑在偏向锁获取流程小节中。2 处理当前正在被使用的锁对象,通过遍历所有存活线程的栈,找到所有正在使用的偏向锁对象,然后更新它们的epoch值。也就是说不会重偏向正在使用的锁,否则会破坏锁的线程安全性。
1. omAlloc(Self)优先会去线程私有ObjectMonitor空闲列表中分配一个ObjectMonitor,如果没有可分配的ObjectMonitor,则会去JVM全局的ObjectMonitor空闲列表中分配2. 初始化已分配的ObjectMonitor对象3. 设置ObjectMonitor的header字段为mark word,owner字段为null,obj字段为锁对象,4. cas操作更新锁对象的mark word为重量锁状态,失败则释放该ObjectMonitor对象成功则指向该ObjectMonitor对象
如果是匿名偏向或者attempt_rebias等于falsemark->is_biased_anonymously() && !attempt_rebias
对于偏向锁的解锁流程很简单,只是将占用的Lock Record释放即可
是为了尽量避免没必要的锁升级
如果对象的mark word偏向标志可偏向 mark->has_bias_pattern()1.类的mark word偏向标志不可偏向 !prototype_header->has_bias_pattern()2.对象的mark word中epoch不等于类的mark word中epoch prototype_header->bias_epoch() != mark->bias_epoch()
批量撤销
mark word用来保存锁对象的mark word,用来锁释放时恢复锁对象原有的mark word的object ref用来指向锁对象
update_heuristics会根据不同情况返回以下四种类型HR_NOT_BIASED---代表对象不可偏向HR_BULK_REVOKE---批量撤销 BiasedLockingBulkRevokeThresholdHR_BULK_REBIAS---批量重偏向 BiasedLockingBulkRebiasThresholdHR_SINGLE_REVOKE---单线程撤销偏向BiasedLockingBulkRebiasThreshold是重偏向阈值(默认20),BiasedLockingBulkRevokeThreshold是批量撤销阈值(默认40),BiasedLockingDecayTime是开启一次新的批量重偏向距离上次批量重偏向的后的延迟时间,默认25000。也就是开启批量重偏向后,经过了一段较长的时间(>=BiasedLockingDecayTime),撤销计数器才超过阈值,那我们会重置计数器。
否
判断epoch_mask_in_place是否过期(anticipated_bias_locking_value & epoch_mask_in_place) !=0
修改第一个Lock Record为无锁状态,然后将obj的mark word设置为执行该Lock Record的指针
如果是无锁状态,设置Displaced Mark Word并替换对象头的mark word,如果是重入,则设置Displaced Mark Word为null
0
如果是重入_recursions++;
无锁状态膨胀
如果是通过轻量锁膨胀之后首次进入初始化_recursions = 1将_owner设置为当前线程
是
BiasedLocking::revoke_at_safepoint(obj);
1
轻量锁的解锁过程就是先从Lock Record中的mark word恢复到锁对象的mark word中,然后再一一释放占用的Lock Record
CAS失失败
1. omAlloc(Self)优先会去线程私有ObjectMonitor空闲列表中分配一个ObjectMonitor,如果没有可分配的ObjectMonitor,则会去JVM全局的ObjectMonitor空闲列表中分配2. 初始化已分配的ObjectMonitor对象3. 设置INFLATING状态的mark word4. cas操作更新锁对象的mark word,尝试将带有INFLATING状态的mark word替换锁对象的mark word5. 设置ObjectMonitor的header字段为displaced mark word,owner字段为Lock Record,obj字段为锁对象6. 将锁对象的锁状态更新为重量锁,并指向分配的ObjectMonitor
将当前线程封装成ObjectWaiterObjectWaiter node(Self); Self->_ParkEvent->reset(); node._prev = (ObjectWaiter *) 0xBAD; node.TState = ObjectWaiter::TS_CXQ;
判断是否偏向当前线程,如果是偏向当前线程则不需要等到安全点,直接调用revoke_bias方法,如果不是偏向当前线程,最终会在VM线程中的safepoint调用revoke_bias方法
获取锁对象的mark word,会有以下几种状态 Inflated - 已经膨胀为重量锁,直接返回 Stack-locked - 轻量锁需要进行膨胀操作 INFLATING - 当前有线程正在执行膨胀操作,则进行忙等待 Neutral - 无锁状态需要进行膨胀操作 BIASED - 非法状态,这里不可能出现,直接无视
撤销单个
无法撤销直接返回撤销单个批量重偏批量撤销
创建一个无状态的mark word -- displaced,并将Lock Record的object ref指向他
利用CAS指令尝试重偏向,创建一个偏向当前线程且epoch为类的epoch的mark word,并将锁对象的mark word替换成新的mark word
Lock Record
自旋尝试获取锁
SyncFlags默认为0,如果没有其他等待的线程,则将_Responsible设置为自己
收藏
0 条评论
下一页