「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。
批量重偏向和批量撤销
从偏向锁的加锁和解锁的过程中可以看出,当只有一个线程反复进入同步代码块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获取锁的时候,就需要等到 safe point 时,再将偏向锁撤销为无锁的状态或者升级为轻量级锁,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提升性能,还会导致性能下降,于是,有了批量重偏向与批量撤销的机制。
以 class 为单位,每个class 维护一个偏向锁撤销计数器,每一次该 class 的对象发生偏向撤销操作时,该计数器 +1 ,当这个值达到重偏向阈值(默认 20 )时,JVM 就认为该 class 的偏向锁有问题,因此会进行批量重偏向。
每个 class 对象会对应一个 epoch 字段,每个处于偏向锁状态的对象的 mark word 也有该字段,其初始值为创建该对象的 class 的 epoch 的值,每次发生批量重偏向时,就将该值 +1 ,同时遍历 JVM 中所有线程的栈,找到该 class 所有正处于加锁状态的偏向锁,将其 epoch 字段更新为新值,下次获得锁时,发现该对象的 epoch 值和 class 的 epoch 值不想等,那就算当前已经偏向了其他的线程,也不会执行撤销操作,而是直接通过 CAS将其 Mark word 的 thread id 修改为当前线程 id
当达到重偏向阈值(默认 20)后, 假设该 class 计数器继续增长,当其达到批量撤销的阈值(默认40) JVM 就认为该 class 的使用场景存在多线程竞争,会标记当前 class 不可偏向,之后,对于该 class 的锁,直接走轻量级锁的逻辑。
运用场景(解决的问题):
1、批量重偏向(bulk rebias) 机制:一个线程创建了大量对象并且执行了初始的同步操作,后来另外一个线程也来将这些对象作为锁进行操作,这样会导致大量的偏向锁撤销操作。
2、批量撤销(bulk revoke) 机制:在明显多线程竞争剧烈的场景下使用偏向锁时不合适的。
Java 系统默认参数查询
java -XX:+PrintFlagsFinal
我们可以通过-XX:BiasedLockingBulkRebiasThreshold 和-XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值。默认值如下:
批量偏向锁重偏向
实验代码如下:
public class A1 {
public static void main(String[] args) throws Exception {
//延时产生可偏向对象
Thread.sleep(5000);
//创造100个偏向线程t1的偏向锁
List<A> listA = new ArrayList<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
A a = new A();
synchronized (a) {
listA.add(a);
}
}
try {
//为了防止JVM线程复用,在创建完对象后,保持线程t1状态为存活
Thread.sleep(100000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
//睡眠3s钟保证线程t1创建对象完成
Thread.sleep(3000);
System.out.println("打印t1线程,list中第20个对象的对象头:");
System.out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable()));
//创建线程t2竞争线程t1中已经退出同步块的锁
Thread t2 = new Thread(() -> {
//这里面只循环了30次!!!
for (int i = 0; i < 30; i++) {
A a = listA.get(i);
synchronized (a) {
//分别打印第19次和第20次偏向锁重偏向结果
if (i == 18 || i == 19) {
System.out.println("第" + (i + 1) + "次偏向结果");
System.out.println((ClassLayout.parseInstance(a).toPrintable()));
}
}
}
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t2.start();
Thread.sleep(3000);
System.out.println("打印list中第11个对象的对象头:");
System.out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
System.out.println("打印list中第26个对象的对象头:");
System.out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
System.out.println("打印list中第41个对象的对象头:");
System.out.println((ClassLayout.parseInstance(listA.get(40)).toPrintable()));
}
}
我们一起来看看结果,在 thread0 中创建的第 20 个共享锁对象,当前是偏向锁,线程 id -594241531
再来看看 t2 线程,前19次均产生了轻量级锁,到了20次时候到达批量重偏向的阈值20 ,此时不是轻量级锁,而变成了偏向锁,此时偏向锁线程 t2 , 线程 id : -619083515
再看看偏向锁结束后的对象头信息。 前 20 个对象并没有触发批量重偏向机制,线程 t2 释放锁之后,变成无锁状态;
第 20 - 30 个对象,触发了批量重偏向机制,对象为偏向锁状态,偏向线程 t2, 线程 t2 的 id 为-619083515
第 31 个对象之后,也没有触发了批量重偏向机制,对象依然偏向线程 t1 , 线程 t1 的 id 是 -594241531
注意:这里建议共享对象为自定义类对象
批量偏向锁撤销
当撤销偏向锁的阈值超过 40 次过后, jvm 会认为不该偏向,于是整个类的所有对象都会变成不可偏向,新创建的对象也不可偏向。
注意: 对于统计批量撤销有个参数:-XX:BiasedLockingDecayTime=25000ms范围内没有达到 40 次,撤销次数清 0 ,重新记时
public class B1 {
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
List<A> listA = new ArrayList<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i <100 ; i++) {
A a = new A();
synchronized (a){
listA.add(a);
}
}
try {
Thread.sleep(100000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(3000);
Thread t2 = new Thread(() -> {
//这里循环了40次。达到了批量撤销的阈值
for (int i = 0; i < 40; i++) {
A a =listA.get(i);
synchronized (a){
}
}
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t2.start();
Thread.sleep(3000);
System.out.println("打印list中第11个对象的对象头:");
System.out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
System.out.println("打印list中第26个对象的对象头:");
System.out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
System.out.println("打印list中第90个对象的对象头:");
System.out.println((ClassLayout.parseInstance(listA.get(89)).toPrintable()));
Thread t3 = new Thread(() -> {
for (int i = 20; i < 40; i++) {
A a =listA.get(i);
synchronized (a){
if(i==20||i==22){
System.out.println("thread3 第"+ i + "次");
System.out.println((ClassLayout.parseInstance(a).toPrintable()));
}
}
}
});
t3.start();
Thread.sleep(10000);
System.out.println("重新输出新实例A");
System.out.println((ClassLayout.parseInstance(new A()).toPrintable()));
}
}
看看输出结果,这部分和上面的批量偏向有些不同。重点关注线程 id
前 20 个对象并没有触发批量重偏向机制,线程 t2 释放锁之后,变成无锁状态;
第 20 - 40 个对象,触发了批量重偏向机制,对象为偏向锁状态,偏向线程 t2, 线程 t2 的 id 为 -1005618939
第 41 个对象之后,也没有触发了批量重偏向机制,对象依然偏向线程 t1 , 线程 t1 的 id 是 -1039208443
t3 线程开始进行锁的竞争,因为已经到到批量撤销的阈值,且对象 listA.get(20) 和 listA.get(22) 已经进行了锁的重偏向了,所以 t3 再次获取锁的时候,不会触发重新偏向为线程 t3.
此刻,触发批量撤销,此对象碰撞变为轻量级锁
最后我们看一下新生成的对象。本来应该是可偏向状态,但是在经历过批量重偏向,和批量撤销之后转换为无锁。
批量重偏向和批量撤销总结
- 批量重偏向和批量撤销针对类的优化,和对象无关。
- 批量锁重新偏向一次过后不能重新偏向。
- 当某个类已经触发批量撤销机制后, JVM 会默认当前类长生了严重的问题,剥夺了该类新生成对象偏向锁的权利。