批量重偏向和批量撤销
从偏向锁的加锁和解锁的过程中可以看出,当只有一个线程反复进入同步代码块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获取锁的时候,就需要等到 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
注意:这里建议共享对象为自定义类对象