一、前言
最近比较堕落,因为找不到工作后,放松了学习。写这篇博客是因为前段时间看八股文的一个synchronized流程图感觉有些地方不合理,于是上网查找其它博客,结合这些博客和源码来了解具体的流程。
二、阅读的博客来源
通过阅读这些博客以及源码,于是我得出以下的流程总结。
三、我的总结之偏向锁篇
首先先获取lock record
,lock record
表向上增长,属于线程栈而不是栈桢;在while循环中获取到的lock record
,要满足:
lock record
是空闲的,也就是指向的对象obj是null;lock record
下面所有的lock record
指向的obj都不能是当前锁对象;- 为的是可重入情况,如果找到一个空闲
lock record
,它的上面有一个lock record
( j )指向的obj就是当前锁对象,说明是重入的;- 这个找到的空闲的
lock record
与 j 这一个lock record
之间的所有lock record
在查找的时候必须都不是空闲的同时指向的obj都不是当前锁对象。 - 这样可以在释放锁对象的时候,释放最低的那一个obj指向当前锁对象的
lock record
即可。
- 这个找到的空闲的
- 为的是可重入情况,如果找到一个空闲
如果获取不到满足条件的lock record
,会重新执行当前锁方法;
偏向锁获取
- 首先将获取到的
lock record
的obj指向设置为当前锁对象。 - 获取当前锁对象的mark,然后判断是否是偏向模式,也即锁标志位为01同时偏向锁位置为1,如果是的话就进入偏向锁的获取,否则就是轻量级锁的获取。进入偏向锁的获取后开始进行下面步骤。
- 通过klass的mark值( a )以及当前要获取锁的线程id( b )以及偏向锁对象的mark( c )进行一些位运算得到一个值x,这个值可以比较a b c三个值的位置上哪些位(比如用来比较a的epoch和c的epoch以及b值和c的线程id位)不同,要注意的是年龄位不比较。
- 假如x为0,说明a的epoch值与c的epoch值一致,同时a的偏向模式开启,而且c的线程位上的线程id值和b值一样,那么说明当前偏向锁对象是偏向当前要获取锁的线程,于是直接跳过代表获取锁成功。
- 假如用x和偏向锁位(这是一个固定值)比较发现不为0(说明a的偏向模式位和c的偏向模式位不一样),此时说明虽然锁对象的偏向锁模式没关,但是klass的偏向锁模式已经关了(此时进行了一次批量撤销),因此需要通过cas将当前偏向锁对象的mark修改为无锁模式,无论cas是否成功都直接膨胀成轻量级锁。
- 无锁模式就是取消了偏向锁模式。
- 假如用x和epoch位(固定值)比较发现不为0(说明a的epoch和c的epoch不一样),此时klass是偏向模式;而且经过了一次批量重偏向,但是在批量重偏向的时候对所有还在被使用的klass的偏向锁实例设置了epoch为新的,因此到了这一步了说明虽然当前偏向锁对象的线程id指向一个其它线程,但是那个其它线程已经释放了这个偏向锁了,因此直接通过cas将当前线程的线程id设置进去即可(此时获取偏向锁成功);cas失败则进入
fast_enter
,会进行撤销与重偏向:撤销成功重偏向失败会膨胀为轻量级锁;撤销成功重偏向成功就成功获得锁。- 注意,epoch改变了说明当前类的类实例作为偏向锁的时候撤销次数太多达到阈值于是epoch+1,于是会进行批量重偏向,epoch超过阈值会进行批量撤销。(在一定时间内超过才算)
- 最后如果是匿名偏向(偏向锁对象的偏向模式开启且mark中的线程id为0),则可以直接cas设置偏向线程id,设置成功获取锁;否则进入
fast_enter
。
偏向锁的释放
- 从线程栈的
lock record
表中低地址往上找找到第一个obj指向偏向锁对象的lock record
,将其释放即可。
偏向锁的撤销
- 假设锁已经偏向线程A,这时B线程尝试获得锁。
- 会将该操作push到VM Thread中等到
safepoint
的时候再执行。因为需要遍历jvm中的java线程集合。- 在安全点里的操作一般不需要cas。
- 查看偏向的线程是否存活,如果已经不存活了,则直接撤销偏向锁(按照
attempt_bias
是true还是false设置匿名偏向或无锁然后返回)。JVM维护了一个集合存放所有存活的线程,通过遍历该集合判断某个线程是否存活。 - 从低往高遍历偏向线程的
lock record
表:- 如果发现有
lock record
的obj指向偏向锁对象,那么将该lock record
的displaced mark
设置为null,并且时刻记录最新的obj指向偏向锁对象的lock record
为high;这一步也说明偏向的线程还在同步块中。 - 如果high为null,说明偏向线程不在同步块中,直接设置为匿名偏向(还是偏向锁模式)或者无锁(升级为轻量级锁)即可。
- 如果high不为null,那么说明偏向线程在同步块里,需要将high(最高的且obj指向偏向锁对象的
lock record
)的displaced mark
设置为无锁模式(不再是偏向锁模式了)。
- 如果发现有
补充:bulk_revoke_or_rebias_at_safepoint这个方法应该也是要在safepoint进行的,因为这个方法的第一行就是assert(SafepointSynchronize::is_at_safepoint(), "must be done at safepoint");
。
批量重偏向
- 偏向锁撤销次数在某一段时间内太多,达到批量重偏向阈值,进行批量重偏向
- 进入
bulk_revoke_or_rebias_at_safepoint
方法,klass的epoch自增。 - 在
safepoint
下,遍历所有线程的所有lock record
,如果遇到lock record
的obj的klass指向klass,那么将这个obj也就是偏向锁对象的mark的epoch设置为klass的epoch。 - 这里有个很有意思的点,偏向线程退出同步块后,不会修改偏向锁对象的mark,只清空
lock record
,所以偏向锁对象的mark上的线程id还是指向刚刚退出同步块的偏向线程,而上一步的遍历是根据lock record
来的,因此批量重偏向的时候偏向锁对象的mark也没有变(主要是epoch没变);但是在另外一个线程t2进入获取该偏向锁的时候,发现klass的epoch与这个偏向锁对象的epoch不一致,说明t2可以直接cas获取这个偏向锁,因为这个偏向锁对象的mark中的线程id指向的线程早已退出同步块。 - 我的理解:因为偏向锁的释放是不会改变偏向锁对象的
mark word
的,但是每次其它线程(不是刚刚获取并释放偏向锁的那个线程)重新获取这个偏向锁的时候,此时才会去撤销偏向锁,而撤销偏向锁的操作是要在安全点里做的,假设对于一个类,它的很多实例对象都会作为偏向锁对象而存在,如果遇到那种一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。
那么可能很多线程在获取偏向锁的时候都会进行偏向锁撤销,然后堵在了safepoint,假设因为短时间内超过阈值而触发批量重偏向(改epoch
),那么这个操作只堵在一次safepoint。
批量撤销
- 偏向锁撤销次数在某一段时间内太多,达到批量撤销阈值。
- 进入
bulk_revoke_or_rebias_at_safepoint
方法,klass设置为无锁模式(非偏向锁)。 - 遍历所有线程的所有
lock record
,遇到klass指向当前klass的偏向锁对象,对其进行revoke_bias
,操作与上面的偏向锁的撤销一样。- 这里的线程都是存活着的,且
attempt_bias
为false,说明都会升级为轻量级锁。
- 这里的线程都是存活着的,且
- 存在明显多线程竞争的场景下使用偏向锁是不合适的,例如生产者/消费者队列。此时在短时间内超过阈值触发批量撤销,那么klass设置无锁。那么当前klass的实例对象被作为锁获取的时候就不会使用偏向锁了。
四、我的总结之轻量级锁篇
轻量级锁的获取
-
在
bytecodeInterpreter
的CASE(_monitorenter)
代码块中,首先先遍历获取可用的lock record
,然后进行偏向锁的获取,失败后进入轻量级锁的获取。 -
首先构建一个当前锁对象相关的无锁标记的
displaced mark word
,然后将lock record
的displaced header
设置为刚刚构建的displaced header
; -
这里暂时不管重量级锁的逻辑。。。
-
通过CAS给锁对象的对象头
mark
设置为lock record
的地址,设置成功则获取轻量级锁成功! -
设置失败则说明:
- 假如当前线程拥有这个锁对象,说明cas失败是因为当前是锁重入,因此直接将刚刚设置为非空的
lock record
的displaced mark header
设置为null;此时获取轻量级锁成功! - 假如当前线程没有这个锁对象,说明cas失败同时不是锁重入,锁对象被其它线程抢到了,此时锁对象可能是轻量级锁,但也可能已经是重量级锁了,也有可能是无锁状态(cas失败后让获取到锁的线程立刻执行完然后退出);于是进行
CALL_VM(...);
,进入fast_enter
方法;进入fast_enter(在revoke_and_rebais
中会进入一个函数update_heuristics(obj(), attempt_rebias);
判断锁对象是不是偏向锁模式,如果不是,则返回NOT_BIASED
状态, 这一步会使得fast_enter
进入slow_enter
),然后进入slow_enter
。(xyz位置)
- 假如当前线程拥有这个锁对象,说明cas失败是因为当前是锁重入,因此直接将刚刚设置为非空的
-
假设进入了
slow_enter
:-
那么先判断锁对象是否是无锁状态,如果是的话可以使用轻量级锁,将
lock record
的displaced mark header
通过cas设置为锁对象的mark word
;- cas成功则获取轻量级锁成功,直接返回!
- cas失败,说明有其他线程抢到了锁对象,进入锁膨胀。
-
如果锁对象是有锁状态而且拥有者是当前线程,说明此时是重入,那么只需要将
lock record
的displaced mark header
设置为null即可,此时获取轻量级锁成功,直接返回!(但是这一步如果是从xyz位置
进入的话应该不会执行到的,毕竟xyz位置
的前一步就设置过了) -
其它情况如锁对象不属于当前线程的情况就进入锁膨胀。
-
// 锁膨胀 lock->set_displaced_header(markOopDesc::unused_mark()); ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
-
轻量级锁的释放
- 从线程栈的
lock record
表中低地址往上找找到第一个obj指向偏向锁对象的lock record
,清空lock record
的obj指向; - 如果当前锁对象头不是偏向锁模式,那么首先判断
lock record
的displaced mark header
是否为null:- 是的话则不需要管,因为此时只是退出重入的情况;释放轻量级锁成功!
- 否则通过cas将锁对象的
mark word
设置为displaced mark word
;(此时不是重入的情况)- cas成功,则释放轻量级锁成功!
- cas失败,说明此时锁对象的头不是指向当前的
lock record
,先将lock record
的obj设置为当前释放的锁对象,然后调用CALL_VM(InterpreterRuntime::monitorexit(THREAD, most_recent), handle_exception);
释放锁。
CALL_VM(InterpreterRuntime::monitorexit(THREAD, most_recent), handle_exception);
:- 进入此方法后,直接进入
synchronezed.cpp
的slow_exit
方法,然后直接进入fast_exit
方法; - 如果是重入锁的情况,则什么都不做;(重入锁的逻辑里全是assert)
- 如果是
mark word==Displaced Mark Word
即轻量级锁,CAS替换锁对象头的mark word
:- cas成功则直接返回,释放锁成功!
- cas失败则进入下一步;
- 走到这里说明是重量级锁解锁或者解锁时发生了竞争,膨胀后调用重量级锁的exit方法。(我个人认为如果从上面的
CALL_VM(...)
进入这里,那么说明不会是重入锁,同时肯定发生了锁竞争,否则一次cas就将锁对象的指向lock record
的mark word
设置为无锁的mark word
了)
- 进入此方法后,直接进入
最后重量级锁看上面那篇博客就行了,那篇博客已经列出升级到重量级锁和重量级锁的加锁以及重量级锁的释放步骤了。