前言
最近看了一些synchronized底层原理的博文,对偏向锁这块儿不是很理解,结合网上一些博主的文章和示例代码,记录下自己的理解。
参考的文章有:
1.程序员囧辉的全网最硬核的 synchronized 面试题深度解析
2.Fisher3652的并发编程:批量重偏向、批量撤销
相关概念
锁升级流程:
对象头组成:
小端模式表示对象头
小端模式:Inter x86、ARM核 采用的是小端模式存储数据(低地址存放低字节数据,高地址存放高字节数据)
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 01 00 00 (00000101 00000001 00000000 00000000) (261)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) ca d8 00 f8 (11001010 11011000 00000000 11111000) (-134162230)
12 4 java.lang.String User.name null
以上对象头中Mark word 4个字节的数据,是按照内存地址由低到高打印的,而内存中的地址是向上增长的,低地址存放低字节数据,高地址存放高字节数据,所以,Mark word实际的数据以16进制表示为:0x00000105
这样便与上图中对象头的 Mark word 的32位数据对应了起来,即锁标志位在低位。
启动偏向锁设置
JVM为我们提供了参数- XX:+UseBiasedLocking以开启或者关闭偏向锁优化(默认开启),但jvm延迟了4s启动偏向锁,想要在程序已启动时,就启动偏向锁,有两种方式:
- 程序启动时,让主线程睡眠超过4s,这样就会在jvm延迟启动偏向锁后,才开始运行程序。
- 设置jvm启动参数,添加-XX:BiasedLockingStartupDelay=0,即设置延迟启动偏向锁的时间为0s,这样jvm启动时,偏向锁就会启动。
批量重偏向
示例代码
创建20个锁对象,创建2个线程A、B,启动线程A,让线程B等待;在线程A中循环获取这20个锁对象;线程A执行完后,唤醒线程B,让线程B循环获取这20个已经偏向线程A的锁对象;
/**
* 偏向锁-批量重偏向测试
* @Author: AntonyZhang
* @Date: 2021/7/6 10:25 下午
**/
@Slf4j
public class BulkRebiasTest {
static Thread A;
static Thread B;
static int loopFlag = 20;
public static void main(String[] args) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
final List<User> list = new ArrayList<>();
A = new Thread() {
@Override
public void run() {
for (int i = 0; i < loopFlag; i++) {
User a = new User();
list.add(a);
log.info("加锁前第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
synchronized (a) {
log.info("加锁中第 {} 次"+ ClassLayout.parseInstance(a).toPrintable(), i+1);
}
log.info("加锁结束第 {} 次"+ ClassLayout.parseInstance(a).toPrintable(), i+1);
}
log.info("============线程A 都是偏向锁=============");
//防止竞争 执行完后唤醒线程B
LockSupport.unpark(B);
}
};
B = new Thread() {
@Override
public void run() {
//防止竞争 先睡眠线程B
LockSupport.park();
for (int i = 0; i < loopFlag; i++) {
User a = list.get(i);
//因为从list当中拿出都是偏向线程A
log.debug("加锁前第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
synchronized (a) {
//20次撤销偏向锁偏向线程A;然后升级轻量级锁指向线程B线程栈当中的锁记录
//后面的发送批量偏向线程B
log.debug("加锁中第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
}
//因为前19次是轻量级锁,释放之后为无锁不可偏向
//但是第20次是偏向锁 偏向线程B 释放之后依然是偏向线程B
log.debug("加锁结束第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
}
log.debug("新产生的对象" + ClassLayout.parseInstance(new User()).toPrintable());
}
};
A.start();
B.start();
}
}
对于线程A,可以看到,20个锁对象的对象头中markword在对象创建后三个阶段的变化情况如下:
加锁前:都是处于可偏向状态
加锁中:线程A获取该锁对象后,锁对象的对象头中的markword字段 保存了线程A的线程ID
加锁后:每个锁对象依然偏向线程A,因为此时还未唤醒线程B
线程A打印如下图所示:
对于线程B,被唤醒后,前19个锁对象在加锁前、锁定中、释放后的三个阶段,对象头中的markword字段变化情况如下(前19次打印结果都一样,取第19次结果):
加锁前:偏向线程A
锁定中:先执行撤销偏向锁流程,然后执行轻量级锁加锁流程,加锁成功后,图中锁标志位为00
释放后:执行轻量级锁解锁流程
线程B打印如下图所示:
对于线程B,第20个锁对象被获取前、锁定中、释放后的三个阶段,对象头中的markword字段变化情况如下:
加锁前:偏向线程A
锁定中:先执行撤销偏向锁流程,偏向锁撤销计数器此时已累计撤销偏向20次,达到了批量重偏向的阈值(默认为20)。因此,便会触发批量重偏向的流程,重偏向后,锁对象偏向了线程B,图中锁标志位为101
释放后:依然偏向线程B
线程B如下图所示:
关于偏向锁的偏向纪元epoch,重偏向前后的变化情况,如下图中markword从低位到高位的
第8和第9个bit位所示:
可见,重偏向后,锁对象的epoch值为10,而重偏向前,锁对象的epoch值为00
重偏向前,偏向线程A的锁对象的markword
高位 低位
01111010 00000100 00111000 00000101
重偏向后第20个锁对象的markword
11011101 00001100 10000001 00000101
重偏向后,新创建的锁对象的markword
00000000 00000000 00000001 00000101
过程总结
偏向锁的批量重偏向过程:
1.匿名偏向:开启偏向锁优化,类的实例对象创建后,尚未被任何一个线程拿到该锁对象,此时锁对象处于可偏向状态。
2.偏向锁:只有一个线程A进入了拿到锁进入同步块,此时,作为锁对象的对象头中markword 的偏向锁标志为1,锁标志位为01。
3.轻量级锁:线程A执行完同步块中任务,退出同步块后(此时,该锁对象依然偏向线程A),由线程B尝试获取步骤2中创建的锁对象,因为此时线程B属于第二个线程,即使线程A已经释放了锁,锁对象还是会撤销偏向并升级为轻量级锁。
1) 撤销偏向锁:线程B尝试获取锁,此时的对象锁依然为偏向锁,所以要执行偏向锁的撤销操作:CAS将锁对象的markword修改为无锁状态,撤销偏向后,偏向锁撤销计数器加1;
2)升级轻量级锁:锁对象的markword为无锁状态,先将锁对象的markword填充到栈中Lock Record的displaced_header字段,然后CAS将栈中Lock Record的地址记录到锁对象的对象头中markword的高30位;若CAS成功,则获取轻量级锁成功,此时对象头的markword锁标志位为00。
4.批量重偏向:先将Klass的prototype_header中的epoch值加1,然后遍历每个栈帧中包含的BasicObjectLock,如果其关联的锁对象oop是该Klass,则增加该oop的对象头中的epoch值,遍历完成后将触发批量重偏向的这个锁对象oop重新偏向给当前线程。
本例中在User类的20个对象的偏向锁都撤销偏向后,偏向锁撤销计数器的值为20,达到了偏向锁批量重偏向的阈值,就会触发批量重偏向。当前线程即为线程B。
批量撤销
示例代码
创建40个锁对象,创建2个线程A、B、C,线程A先执行,让线程B等待线程A执行结束后再执行,让线程C等待线程B执行结束后再执行;
在线程A中循环获取这40个锁对象;线程A执行完后,唤醒线程B,让线程B循环获取这40个已经偏向线程A的锁对象;
线程B执行完后,唤醒线程C,让线程C循环获取这40个锁对象(根据上面👆🏻批量重偏向的测试可知,此时的锁对象,前19个为无锁不可偏向,从第20个锁对象批量重偏向后,后21个为偏向线程B的锁对象)
/**
* 偏向锁-批量撤销测试
* @Author: AntonyZhang
* @Date: 2021/7/6 10:25 下午
**/
@Slf4j
public class BulkRevokeTest {
static Thread A;
static Thread B;
static Thread C;
static int loopFlag = 40;
public static void main(String[] args) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
final List<User> list = new ArrayList<>();
A = new Thread(() -> {
for (int i = 0; i < loopFlag; i++) {
User a = new User();
list.add(a);
log.info("加锁前第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
synchronized (a) {
log.info("加锁中第 {} 次"+ ClassLayout.parseInstance(a).toPrintable(), i+1);
}
log.info("加锁结束第 {} 次"+ ClassLayout.parseInstance(a).toPrintable(), i+1);
}
log.info("============线程A 都是偏向锁=============");
//防止竞争 执行完后叫醒 线程B
LockSupport.unpark(B);
});
B = new Thread(() -> {
//防止竞争 先睡眠线程B
LockSupport.park();
for (int i = 0; i < loopFlag; i++) {
User a = list.get(i);
//因为从list当中拿出都是偏向线程A
log.debug("加锁前第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
synchronized (a) {
//40次撤销偏向锁偏向线程A;然后升级轻量级锁指向线程B线程栈当中的锁记录
//后面的发送批量偏向线程B
log.debug("加锁中第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
}
//因为前19次是轻量级锁,释放之后为无锁不可偏向
//但是第20次是偏向锁 偏向线程B 释放之后依然是偏向线程B
log.debug("加锁结束第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
}
log.debug("新产生的对象" + ClassLayout.parseInstance(new User()).toPrintable());
//防止竞争 执行完后叫醒 线程C
LockSupport.unpark(C);
});
C = new Thread(() -> {
//防止竞争 先睡眠线程C
LockSupport.park();
for (int i = 0; i < loopFlag; i++) {
User a = list.get(i);
log.debug("加锁前第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
// 偏向撤销次数已达到批量撤销阈值40,则执行批量撤销流程
synchronized (a) {
log.debug("加锁中第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
}
log.debug("加锁结束第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
}
log.debug("新产生的对象" + ClassLayout.parseInstance(new User()).toPrintable());
});
A.start();
B.start();
C.start();
}
}
线程B在升级为轻量级锁时,会对40个锁对象先执行偏向锁撤销流程,这样,而重偏向后,锁对象偏向线程B,偏向撤销计数器的值为40,达到了批量撤销的默认阈值,此时便会执行偏向锁的批量撤销流。
批量撤销:将 class 的 markword 修改为不可偏向无锁状态,也就是偏向标记位为0,锁标记位为01。接着遍历所有当前存活的线程的栈,找到该 class 所有正处于偏向锁状态的锁实例对象,执行偏向锁的撤销操作。
这样当线程后续尝试获取该 class 的锁实例对象时,会发现锁对象的 class 的 markword 不是偏向锁状态,知道该 class 已经被禁用偏向锁,从而直接进入轻量级锁流程。
线程C打印如下图:
可以看出,线程C在获取锁对象前,该锁对象已经是无锁不可偏向状态,锁定后,锁对象为轻量级锁,解锁后,恢复为无锁不可偏向状态;且新创建的锁对象仍然为无锁不可偏向状态,因为JVM任务该类的新实例对象已经不再适用偏向锁。