1. 偏向锁执行流程
参考&图源:偏向锁_做一个坏人的博客-CSDN博客
当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。
在JVM启动时,偏向锁是延时初始化的,默认是4000ms,在这4秒创建的对象则为无锁状态 。而对于无锁状态的锁对象,如果有竞争,会直接进入到轻量级锁(因为JVM启动时会有大量的同步操作,所以跳过偏向锁,防止产生大量的偏向锁撤销,使性能降低)。
XX:BiasedLockingStartupDelay=4000
所以一般写例子会先Thread.sleep(5000),这样确保在此之后创建的锁对象是匿名偏向锁状态。
访问流程:
-
线程1首次访问同步块,先检测对象头的锁标志位是否为
01
,代表对象锁处于无锁或偏向锁状态。 -
检查偏向锁标志位是否为
1
,若不是,说明未开启偏向锁或者已经升级为轻量级,故进入轻量级锁逻辑。 -
判断为偏向锁后,检查对象头的 Mark Word 的
Thread ID
是否为本线程 ID。-
是,则表明当前线程已经获得对象锁,直接进入同步块,并且往当前线程的栈中添加一条Displaced Mark Word为空的 Lock Record(锁记录),用来统计重入的次数(如图为当对象所处于偏向锁时,当前线程重入3次,线程栈帧中Lock Record记录)。
偏向锁重入
退出同步块释放偏向锁时,则依次删除对应Lock Record,但是不会修改对象头中的Thread Id;
注:偏向锁撤销是指在获取偏向锁的过程中因不满足条件导致要将锁对象改为非偏向锁状态, 而偏向锁释放是指退出同步块时的过程。 -
-
若对象头的线程 ID不是当前线程ID,则会进行
CAS
操作,企图将 Mark Word 的 线程ID 替换成当前线程ID。如果当前对象锁状态处于匿名偏向锁状态(可偏向未锁定),则会替换成功(将Mark Word中的Thread id由匿名的0
改成当前线程ID,在当前线程栈中找到内存地址最高的可用Lock Record,将线程ID存入),获取到锁,执行同步代码块; -
如果对象锁已经被其他线程占用,则会替换失败,开始进行偏向锁撤销,这也是偏向锁的特点,一旦出现线程竞争,就会撤销偏向锁;
-
偏向锁撤退需要等待全局安全点,暂停持有偏向锁的线程,检查持有偏向锁的线程状态。如果线程存在并且还在同步代码块中,则升级为轻量级锁。
-
如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向,如果还未允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁;
-
如果允许重偏向,设置为匿名偏向锁状态,CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID);
-
唤醒暂停的线程,从安全点继续执行代码。
2. 批量重偏向和批量撤销
注意:以下阈值针对的是同一个类的不同实例对象加锁的情况。(如Student类创建了19个实例对象作为锁,都执行了锁撤销,那么Student的第20个对象锁就会执行重偏向)
批量重偏向
首先,一开始并不会开启重偏向(意味着一有竞争就会撤销偏向锁升级为轻量级锁),当累计撤销次数达到阈值次数(20次)的时候,才会允许重偏向(设为匿名偏向状态,线程能够获取到偏向锁),重置对象头的 Thread ID。
所以前19次的锁撤销都会把锁升级成轻量级锁(无法逆转),之后的同类偏向锁对象才会执行重偏向
批量撤销
批量重偏向的重偏向只有一次机会,启动重偏向后累计执行撤销的次数达到阈值(40次,也就是重偏向后再20次)后,批量撤销,当前类的所有对象锁均升级轻量级锁,新创建的该类对象头也是无锁不可偏向状态。
epoch
epoch机制使得批量重偏向的重偏向只能执行一次
-
每次撤销数量刚到20的时候,锁的类epoch都会
+1
,并且更新加锁状态中的此类锁对象(批量行为),那么那些不加锁的锁对象epoch就和类的epoch不一样了,那就可以知道哪些偏向锁是空闲的了。 -
进行偏向时,直接就可以更改线程 ID(因为已知当前锁指向的线程ID未运行),并且将对象epoch设置成类epoch,就标志着这个锁对象已经重偏向过一次了。
再次批量重偏向?
可以设置重置时间,超时(25s)未达到批量撤销的阈值(40次)时,会重置该类的撤销次数计数器,因此批量重偏向可以进行多次。