synchronized_2_无锁偏向锁

190 阅读42分钟

无锁和偏向的关系

无锁是偏向锁的反例,它代表了不可偏向的偏向锁,因此无锁和偏向锁的锁标志位都是01,单独用了一位来标识是否可偏向。无锁状态下线程获取锁会直接采用轻量级锁方式获取锁。

对象头锁标志位问题

锁对象相关锁状态

锁状态如下图:

在32位机器上:

image-20220511194401978-16522694451051.png

以上这个图简单整理了一下,想要看详情可以看JVM源码https://github.com/openjdk/jdk8/blob/master/hotspot/src/share/vm/oops/markOop.hpp,这里面有详细介绍在32位和64位机器里面对象头状态。也可以看JVM文档:https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf

image-20220514093758494-16524922802301.png

我们注意到判断锁对象是哪一种锁,主要是通过 锁是否可偏向标志位+锁标志位 实现的,JVM会通过判断不同的锁状态,从而执行相应的锁逻辑的。

这里需要注意的是偏向锁标志位,对应了无锁和偏向锁,我认为在锁升级中这两种状态是最难的。

有关锁标志位问题?

提出问题?

1、对象创建是无锁还是偏向锁?

2、偏向锁的epoch是干嘛的?

3、无锁状态执行锁升级是升级到偏向锁吗?

对象创建是无锁还是偏向锁

可能有人说,你这不是明摆着无锁状态嘛,对象刚创建都没有加锁,哪来的其它锁状态。执行了锁升级后不就是偏向锁嘛,简单so easy。

网上大多数博客都说锁升级流程锁状态是 无锁->偏向锁->轻量级锁->重量级锁状态升级,但是其实是有一定出入的,这个流程代表了锁的竞争强度,并不能代表锁升级流程就是这样。

创建对象默认到底是无锁还是偏向锁?

答:当一个对象创建的时候要看该对象创建的时候是否已经启动了偏向锁,如果没有启动则对象锁状态为无锁状态,如果已经启用了偏向锁,则对象锁状态为偏向锁状态。

证明

Java默认是开启了偏向锁的,并且启动了延时启动偏向锁配置,默认延时4秒钟。

ps:可以通过配置JVM参数 -XX:+PrintFlagsFinal 查看默认启动的相关JVM参数。

 bool UseBiasedLocking                         = true 启动偏向锁
 intx BiasedLockingStartupDelay                = 4000 延时启动时间

不修改任何默认参数,直接创建对象,并打印对象头:

 SynchronizedDemo lock = new SynchronizedDemo();
 System.out.println(ClassLayout.parseInstance(lock).toPrintable());

结果:

image-20220513194154002.png

可以看到锁标志位为001,代表无锁状态,因为是JVM启动立马就创建了对象,这时候偏向锁还没有开启,因此为无锁状态。


等待偏向锁启动后再创建对象:

 TimeUnit.SECONDS.sleep(5);// 等待启动偏向锁
 SynchronizedDemo lock = new SynchronizedDemo();
 System.out.println(ClassLayout.parseInstance(lock).toPrintable());

结果:

image-20220513194435705.png

可以看到,当偏向锁开启后对象创建默认状态就变为了101状态。我们还可以使用JVM参数 -XX:BiasedLockingStartupDelay=0 禁止延时启动偏向锁,不再sleep(5)再测试会发现对象创建状态依旧为偏向锁。


网上还有些博客,代码是这样写的

 SynchronizedDemo lock = new SynchronizedDemo();
 TimeUnit.SECONDS.sleep(5);
 System.out.println(ClassLayout.parseInstance(lock).toPrintable());

然后测试结果,打印出来对象是无锁状态,然后就下了结论对象创建默认状态是无锁状态,只有线程访问了才会是偏向锁状态,这个结论毫无疑问是错误的

为什么呢?

看上面代码,创建对象的代码并没有在5秒延时后创建,而是在应用一启动就创建了,这时候偏向锁启动了吗?肯定没有启动嘛,因为默认偏向锁要4秒延时启动,因此这里创建的对象肯定是无锁的,只是延时了5秒后打印了对象头状态而已。还有就是,当有一个线程获取了该锁后,该锁就变为了偏向锁,这更不对了,这个下面会证明。

结论

通过上面实验我们可以得出结论,当一个对象创建的时候它的锁标志位可能会存在两种状态,一种是无锁状态一种是偏向锁。这取决于创建对象的时候是否已经启动了偏向锁。

因此理解偏向锁和无锁的区别要从是否能偏向来理解,而不是把它们划分成两个独立的锁,它们都是偏向锁,只是是否能够偏向的问题。

偏向锁的epoch是干嘛的?【重偏向】

我们看到上面上面的无锁和偏向锁的主要区别在于它们有个标志位用于标志是否锁是否能够偏向,偏向锁里面除了存储偏向锁的偏向线程ID之外,还有个epoch信息,这个Epoch是来干嘛的呢?

答:epoch这个标志位是用来判断偏向锁对象是否可重偏向的重要依据,在锁对象和Class都有这个标志位,当偏向锁的epoch和klass的epoch不一致的时候,代表已经重偏向,一致的时候代表当前偏向锁代表可重偏向。

无锁状态执行锁升级是升级到偏向锁吗?

无锁状态升级是升级到偏向锁吗?

答:无锁状态并不会升级成偏向锁,无锁会直接升级成轻量级锁,偏向锁执行锁升级的时候也是先撤销锁状态为无锁后再升级的偏向锁。

ps:再次强调,无锁和偏向锁并不是两种锁,都是偏向锁,只是状态是否能够偏向。因此无论是无锁还是偏向锁它们升级都是轻量级锁

image-20220520194524827-16530471260781.png

证明

无锁状态升级成轻量级锁

无锁状态,观察锁升级(没有加任何JVM参数,没有加任何等待阻塞的代码)通过上面问题证明可以知道创建的对象是无锁状态的:

 SynchronizedDemo lock = new SynchronizedDemo();
 System.out.println("对象头初始化状态:");
 System.out.println(ClassLayout.parseInstance(lock).toPrintable());
 ​
 System.out.println("加锁.......");
 synchronized (lock){
     System.out.println(ClassLayout.parseInstance(lock).toPrintable());
 }
 System.out.println("释放锁.......");
 System.out.println(ClassLayout.parseInstance(lock).toPrintable());

结果:

 对象头初始化状态:
 com.example.demo.blogs.SynchronizedDemo object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
 ​
 ​
 加锁.......
 com.example.demo.blogs.SynchronizedDemo object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           d0 f7 2f a4 (11010000 11110111 00101111 10100100) (-1540360240)
 ​
 释放锁.......
 com.example.demo.blogs.SynchronizedDemo object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)

如何查看锁状态就不再说明了,上面有图片标红的三位就是锁标志位

可以看到,当对象创建的时候锁状态为001,线程持有锁后就变为了000,也就是从无锁变为了轻量级锁,释放后又变回了无锁,这里还能证明出偏向锁是会主动释放锁的。


偏向锁生效

偏向锁状态,观察锁升级(没有加任何JVM参数,加等待阻塞5秒的代码)通过上面问题证明可以知道创建的对象是偏向锁状态的:

偏向锁生效,让线程持有偏向锁:

 TimeUnit.SECONDS.sleep(5);
 SynchronizedDemo lock = new SynchronizedDemo();
 System.out.println("对象头初始化状态:");
 System.out.println(ClassLayout.parseInstance(lock).toPrintable());
 ​
 System.out.println("加锁.......");
 synchronized (lock){
     System.out.println(ClassLayout.parseInstance(lock).toPrintable());
 }
 System.out.println("释放锁.......");
 System.out.println(ClassLayout.parseInstance(lock).toPrintable());

结果:

 对象头初始化状态:
 com.example.demo.blogs.SynchronizedDemo object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
 ​
 加锁.......
 com.example.demo.blogs.SynchronizedDemo object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           05 28 7c 0e (00000101 00101000 01111100 00001110) (243017733)
 ​
 释放锁.......
 com.example.demo.blogs.SynchronizedDemo object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           05 28 7c 0e (00000101 00101000 01111100 00001110) (243017733)

可以看到,当对象创建的时候锁状态为101,线程持有锁后状态不变还是101,但是会发现对象头多了些数据,这些为偏向线程数据,并且释放了锁后这些数据还是在的。ps:这里就又引出了一个问题,持有偏向锁线程是不会主动释放偏向锁,也就是不会清理自己偏向自己锁的偏向数据的,这个后面再说。

偏向锁升级成轻量级锁

首先确定偏向锁升级时机,当线程获取偏向锁的时候,发现偏向锁已经偏向的情况下,会去遍历当前偏向的线程是否存活,如果存活则升级为轻量级锁,如果不存活直接修改偏向为当前线程。

image-20220521095932550.png 正式测试偏向锁升级:

偏向锁已偏向线程死亡,第二个线程直接获取到偏向锁:

 // 休眠5秒钟,创建一个偏向锁
 TimeUnit.SECONDS.sleep(5);
 Object lock = new Object();
 System.out.println(Thread.currentThread().getName() + " 锁创建状态" + ClassLayout.parseInstance(lock).toPrintable());
 // t1获取偏向锁
 Thread t1 = new Thread(() -> {
     synchronized (lock) {
         System.out.println(Thread.currentThread().getName() + " 第一次加锁状态:" + ClassLayout.parseInstance(lock).toPrintable());
     }
 },"Thread1");
 t1.start();
 // 等待t1偏向锁执行完
 TimeUnit.SECONDS.sleep(5);
 System.out.println(Thread.currentThread().getName() + " 锁释放状态" + ClassLayout.parseInstance(lock).toPrintable());
 ​
 Thread t2 = new Thread(() -> {
     synchronized (lock) {
         System.out.println(Thread.currentThread().getName() + " 第二次加锁状态" + ClassLayout.parseInstance(lock).toPrintable());
     }
     try {
         TimeUnit.SECONDS.sleep(100);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
 },"Thread2");
 t2.start();
 ​
 TimeUnit.SECONDS.sleep(100);

测试代码说明:

启动一个线程获取锁,等待一段时间,充分让第一个线程执行完,后打印锁释放状态,发现对象头和第一次加锁状态一样,证明偏向锁在释放的时候不会主动撤销,然后第二个线程再次获取已经偏向的偏向锁,并且偏向线程已终止的情况下,会获取偏向锁成功并修改偏向线程信息。

结果:

 main 锁创建状态java.lang.Object object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
       4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
       8     4        (object header)                           dd 01 00 f8 (11011101 00000001 00000000 11111000) (-134217251)
      12     4        (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
 ​
 Thread1 第一次加锁状态:java.lang.Object object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           05 70 e9 fe (00000101 01110000 11101001 11111110) (-18255867)
       4     4        (object header)                           29 02 00 00 (00101001 00000010 00000000 00000000) (553)
       8     4        (object header)                           dd 01 00 f8 (11011101 00000001 00000000 11111000) (-134217251)
      12     4        (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
 ​
 main 锁释放状态java.lang.Object object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           05 70 e9 fe (00000101 01110000 11101001 11111110) (-18255867)
       4     4        (object header)                           29 02 00 00 (00101001 00000010 00000000 00000000) (553)
       8     4        (object header)                           dd 01 00 f8 (11011101 00000001 00000000 11111000) (-134217251)
      12     4        (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
 ​
 Thread2 第二次加锁状态java.lang.Object object internals: 第二次加锁后,锁对象头值变了18255859
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           0d 70 e9 fe (00001101 01110000 11101001 11111110) (-18255859)
       4     4        (object header)                           29 02 00 00 (00101001 00000010 00000000 00000000) (553)
       8     4        (object header)                           dd 01 00 f8 (11011101 00000001 00000000 11111000) (-134217251)
      12     4        (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
 ​

偏向锁已偏向线程未死亡,第二个线程直接获取到轻量级锁:

 // 休眠5秒钟,创建一个偏向锁
 TimeUnit.SECONDS.sleep(5);
 Object lock = new Object();
 System.out.println(Thread.currentThread().getName() + " 锁创建状态" + ClassLayout.parseInstance(lock).toPrintable());
 // t1获取偏向锁
 Thread t1 = new Thread(() -> {
     synchronized (lock) {
         System.out.println(Thread.currentThread().getName() + " 第一次加锁状态:" + ClassLayout.parseInstance(lock).toPrintable());
     }
     System.out.println(Thread.currentThread().getName() + " 第一次释放锁状态:" + ClassLayout.parseInstance(lock).toPrintable());
     try {
         TimeUnit.SECONDS.sleep(10);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
 },"Thread1");
 t1.start();
 TimeUnit.SECONDS.sleep(3);
 ​
 Thread t2 = new Thread(() -> {
     synchronized (lock) {
         System.out.println(Thread.currentThread().getName() + " 第二次加锁状态" + ClassLayout.parseInstance(lock).toPrintable());
     }
     System.out.println(Thread.currentThread().getName() + " 第二次加锁也释放了" + ClassLayout.parseInstance(lock).toPrintable());
     try {
         TimeUnit.SECONDS.sleep(100);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
 },"Thread2");
 t2.start();
 TimeUnit.SECONDS.sleep(100);

测试代码说明:

比在上面证明多了个让第一个线程等待,不死亡的代码,第二线程获取锁的时候就变为了轻量级锁

 main 锁创建状态java.lang.Object object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
       4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
       8     4        (object header)                           dd 01 00 f8 (11011101 00000001 00000000 11111000) (-134217251)
      12     4        (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
 ​
 Thread1 第一次加锁状态:java.lang.Object object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           05 e8 2d 05 (00000101 11101000 00101101 00000101) (86894597)
       4     4        (object header)                           b8 02 00 00 (10111000 00000010 00000000 00000000) (696)
       8     4        (object header)                           dd 01 00 f8 (11011101 00000001 00000000 11111000) (-134217251)
      12     4        (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
 ​
 Thread1 第一次释放锁状态:java.lang.Object object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           05 e8 2d 05 (00000101 11101000 00101101 00000101) (86894597)
       4     4        (object header)                           b8 02 00 00 (10111000 00000010 00000000 00000000) (696)
       8     4        (object header)                           dd 01 00 f8 (11011101 00000001 00000000 11111000) (-134217251)
      12     4        (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
 ​
 Thread2 第二次加锁状态java.lang.Object object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           a8 f6 af b1 (10101000 11110110 10101111 10110001) (-1313868120)
       4     4        (object header)                           cb 00 00 00 (11001011 00000000 00000000 00000000) (203)
       8     4        (object header)                           dd 01 00 f8 (11011101 00000001 00000000 11111000) (-134217251)
      12     4        (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
 ​
 Thread2 第二次加锁也释放了java.lang.Object object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
       4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
       8     4        (object header)                           dd 01 00 f8 (11011101 00000001 00000000 11111000) (-134217251)
      12     4        (loss due to the next object alignment)
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,当第一个线程获取锁的时候,锁标志位为101偏向锁,第二个线程获取的时候变为了000轻量级锁,因此证明了偏向锁在偏向线程未终止的情况下会升级到轻量级锁。

还有一点,看到这里轻量级锁释放锁了之后,变为了无锁状态,而不是偏向锁状态,并且再次加锁就是从无锁状态加锁会直接使用轻量级锁【不是绝对的,这里还涉及到重偏向的问题】。这个问题也是等到后面分析。

另外:上面的流程图,还说到了偏向锁要升级到轻量级锁还要经历一个撤销的逻辑,这个我们怎么证明呢?

结果就是,我证明不了(嘿嘿),因为它撤销后马上进入了轻量级锁逻辑,因此从锁标志位是看不到的,那么只能看源码。

偏向锁要升级到轻量级锁先要撤销到无锁状态

从头开始看,加锁源码为:InterpreterRuntime::monitorenter(眼熟吧,synchronized加锁进入指令)

 // InterpreterRuntime::monitorenter
 // 是否开启了偏向锁,肯定开了的嘛,直接走fast_enter
 if (UseBiasedLocking) {
 ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
 } else {
 ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
 }
 // ======================================================================================
 // ObjectSynchronizer::fast_enter -> synchronizer.cpp[ObjectSynchronizer::fast_enter]
 void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 // 开启了偏向锁
 if (UseBiasedLocking) {
 // 是否在全局安全点,我们上面的代码,肯定不在,所以直接走这个if里面
 if (!SafepointSynchronize::is_at_safepoint()) {
 // 看到这里了吗:revoke_and_rebias 撤销并且重偏向(嘿嘿,找到了第二次加锁偏向锁要撤销了吧)
 BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
 ....
 } else {
     .......
 }
     ......
 }
     .......
 }
 // ======================================================================================
 // 从上面已经看到了,要撤销偏向锁,那么为什么是撤销成为了无锁,而不是撤销成偏向状态锁呢?就喜欢这种杠精,我们继续跟源码
 // 找到BiasedLocking::revoke_and_rebias,东西太多,就贴点关键的
 ​
 // 判断是否是偏向锁模式
 HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);
 // 不是偏向锁,直接返回了
 if (heuristics == HR_NOT_BIASED) {
     return NOT_BIASED;
     // 单个偏向执行撤销
 } else if (heuristics == HR_SINGLE_REVOKE) {
     Klass *k = obj->klass();
     markOop prototype_header = k->prototype_header();
     // mark->biased_locker() == THREAD:偏向锁持有者是否是当前线程(肯定不是嘛,我们假设的释放后再获取锁的),所以不看了
     if (mark->biased_locker() == THREAD &&
       prototype_header->bias_epoch() == mark->bias_epoch()) {
     ......
     } else {
       // 撤销,执行撤销了,调用线程的doit方法,可以将 VM 线程的 doit 方法类比于 Java 线程的 run 方法
       VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
       VMThread::execute(&revoke);
       return revoke.status_code();
     }
 }
 // =========================================================================================
 // 开始执行撤销了  biasedLocking.cpp::doit
 virtual void doit() {
 if (_obj != NULL) {
 if (TraceBiasedLocking) {
   tty->print_cr("Revoking bias with potentially per-thread safepoint:");
 }
 // 锁对象不为 NULL,调用 revoke_bias 撤销偏向,记住,这里传了两个false
 _status_code = revoke_bias((*_obj)(), false, false, _requesting_thread);
 clean_up_cached_monitor_info();
 return;
 } else {
 if (TraceBiasedLocking) {
   tty->print_cr("Revoking bias with global safepoint:");
 }
 BiasedLocking::revoke_at_safepoint(_objs);
 }
 }
 // ===============================================================================================
 //  biasedLocking.cpp::revoke_bias 方法,其它代码不看了就看这里
 // 246行开始,能走到这里,说明偏向锁偏向线程还存活并且没有持有线程,并且allow_rebias=false
 // 执行obj->set_mark(unbiased_prototype);设置推向头mark为不可偏向状态 大功告成
 if (allow_rebias) {
 obj->set_mark(biased_prototype);
 } else {
 // Store the unlocked value into the object's header.  设置对象mark为不可偏向状态
 obj->set_mark(unbiased_prototype);
 }
 return BiasedLocking::BIAS_REVOKED;
 // ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 至此已经分析完了撤销偏向锁,在偏向线程未持有偏向锁且存活的时候撤销为无锁状态
 // 可把我牛逼坏了,叉会儿腰。如果说你还要证明为什么无锁直接升级为了轻量级锁,这个可以直接创建一个无锁状态的所对象,过一遍synchronized即可看到状态,自己试试就可以了,这个可以根据锁标志位分析出来的,看源码有点多。

从以上源码分析,证明了线程获取偏向锁的时候,在偏向锁已偏向线程未终止的情况下会将对象头设置为无锁状态 obj->set_mark(unbiased_prototype); 也就是上面分析代码的最后一个逻辑行。

最后我们怎么证明它进入了轻量级锁呢?一呢根据锁状态我们已经看出来了,二依旧可以分析源码。

源码分析升级为轻量级锁逻辑:

 // 还是从InterpreterRuntime::monitorenter开始分析
 void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
  if (UseBiasedLocking) {
     if (!SafepointSynchronize::is_at_safepoint()) {
       // 上面分析了锁撤销:最后返回了个啥还记得不:BiasedLocking::BIAS_REVOKED;(偏向已撤销)
       BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
       // cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED?
       // 这里能相等吗?肯定等不了,上面撤销逻辑返回的是BiasedLocking::BIAS_REVOKED
       // 这里是撤销并重定向,要返回这个状态涉及到重偏向,等会再说,大多数情况都不会返回这个状态
       if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
         return;
       }
     } else {
       assert(!attempt_rebias, "can not rebias toward VM thread");
       BiasedLocking::revoke_at_safepoint(obj);
     }
     assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  }
 // 上面都不相等了,那是不是得乖乖的执行这个slow_enter了,其实这里就是锁升级的入口,包括锁膨胀等,后面分析加轻量级锁再看
  slow_enter (obj, lock, THREAD) ;
 }

通过上面分析,偏向锁要到轻量级锁,肯定是要执行锁撤销成无锁,然后再进入加锁逻辑(slow_enter),因为通过锁标志位发现了轻量级锁,因此盲猜,偏向锁后撤销无锁后升级第一个步骤是升级到轻量级锁。具体是不是后面再分析。

结论

无锁执行锁升级是升级到轻量级锁,不会升级到偏向锁,因为偏向锁和无锁本来就是同一个状态的锁。

批量重偏向

什么是批量重偏向

批量重偏向就是指的当一个类创建的偏向锁对象被撤销次数太多,达到一定阈值的时候,默认20,重新回归偏向锁功能。

上面我们分析了,当偏向锁被获取后,获取线程未终止并释放了锁,第二线程再获取这个偏向锁的时候回造成锁撤销,偏向锁撤销代价很大,要防止频繁偏向锁撤销,这也是为什么偏向锁要等待4秒后再启动的原因。

为什么要使用重偏向?

重偏向主要是解决批量偏向锁撤销问题,注意这里关键字是批量锁撤销。当锁对象被做为偏向锁使用之后,使用线程没有结束,另外一个线程来获取该锁的时候,会进行撤销偏向锁成无锁,升级为轻量级锁执行。而偏向锁撤销需要等待Safe Point,造成STW,因此默认情况下锁对象作为偏向锁使用后,就不会再被使用偏向锁使用了,而是直接作为轻量级锁执行。

ps:这里特别要注意,持有偏向锁线程没有死亡的情况下才会造成偏向锁撤销,如果持有偏向锁已经死亡了,则另外一个线程来获取偏向锁则会直接CAS替换成当前线程。

如下案例:

 List<Lock> locks = new ArrayList();
 new Thread(()->{
     for(int i=0;i<100;i++){
         Lock lk = new Lock();
         synchronized(lcok){
             doSomething()
         }
         locks.add(locks);
     }
     TimeUnit.SECONDS.sleep(10000);
 }).start();
 // 第二个线程
 new Thread(()->{
     for(int i=0;i<100;i++){
          Lock lk = locks.get(i);
         synchronized(lcok){
             doSomething()
         }
         locks.add(locks);
     }
 }).start();
 ​

如上所示,第一个线程创建了100个偏向锁,然后呢用了一次就不用了,导致这些偏向锁都处于已偏向但是偏向线程未终止的情况。第二线程再次获取这些偏向锁,就要进行锁撤销,以轻量级锁的方式获取,造成了极大的性能浪费。重偏向能够让第二个线程,直接将这些锁偏向它,而不执行锁撤销就可以达到目的。但是得注意,这些锁一定是基于同一个类创建的。

就比如某个地方100个女人结婚了,然后这100个女人结完婚后,过了新婚之夜后,她们老公后面都不管它们了,她们就像离婚。但是呢,她们老公又还没有离婚,因此要先去离婚(锁撤销)再去结婚。然后呢当地的人就发现,反正它们就是要离婚换老公,还要一个一个离婚太麻烦了,就找个时间点,统计一下要换老公的,统一给它办离婚了(批量标注锁对象可重偏向),然后你爱跟谁接跟谁接。

批量重偏向原理

批量重偏向原理 主要是基于对象和klass中的epoch字段实现的,简单来说就是当触发了重偏向阈值后,通过改变class中的klass中的epoch值,并同时修改所有基于该class生成偏向锁对象的epoch,使它们保持一致,从而达到重偏向的目的,并且基于该class生成的新偏向锁对象也是支持重偏向的。

举例:

第一次线程1获取这100个偏向锁的时候,这时候这100个lock的epoch的值0,并且这100个偏向锁都偏向线程1,并且线程1持有者100个中的其中30个不释放,只释放其中70个锁。

线程2获取线程1已经释放了的70个锁,撤销数量达到了20,触发重偏向逻辑,klass的epoch+1,遍历线程栈帧,将正在被线程持有的基于该class生成偏向锁的epoch和klass的epoch保持一致,代表被线程1持有的这30个锁已经是重偏向了。线程2此时继续获取剩下的50个偏向锁,这50个偏向锁的epoch没有修改和klass不一致,因此这50个偏向锁可重偏向,并且重偏向后epoch也会改成和klass的epoch一致,因此只能一次重偏向。以后基于该class生成的对象,epoch都会和klass的一致,也就是说后面生成的对象只能偏向线程2,否则就会执行锁撤销。 image-20220520215520606-16530549228723.png image-20220521123719087.png

经过上面分析,总结一下:

这里的批量重偏向,批量指的是什么?

答,批量重偏向,批量指的是那些被基于该Class生成的偏向锁,正在被其它线程持有没有释放的这些偏向锁。要将这些偏向锁批量修改其epoch值,改成重偏向状态。

如何判断偏向锁是否能够重偏向?

答:比较当前偏向锁的epoch是否和klass一致,如果一致说明已经重偏向过了,如果线程ID还不是当前线程,则直接撤销,如果线程ID是当前线程,则继续持有偏向锁。(注意:在没有触发批量撤销的情况下)如果epoch和klass不一致,则说明偏向锁可以重偏向,当前线程能够获取该锁。以上结论都是已经在撤销次数达到了阈值的情况,也就是触发了重偏向。

批量重证明

首先明确重偏向触发条件:

可以通过配置JVM参数 -XX:+PrintFlagsFinal 查看默认启动的相关JVM参数。

  • intx BiasedLockingBulkRebiasThreshold = 20 默认偏向锁批量重偏向阈值
  • intx BiasedLockingBulkRevokeThreshold = 40 默认偏向锁批量撤销阈值

重偏向测试步骤:

创建50个偏向锁,都偏向线程Thread1,Thread1保持存活

Thread2等待Thread1锁对象都释放后获取锁对象,查看前20个锁对象是否是轻量级锁,20-40是否是偏向锁(也就是重偏向)

 Thread.sleep(5000);
 List<Object> list = new ArrayList<>();
 // 线程1对50个偏向锁加锁,使得偏向锁偏向线程1,这里创建的50个对象一定是偏向锁,如果不知道为什么可以看前面的对象头锁标志位的介绍
 Thread thread1 = new Thread(() -> {
     for (int i = 0; i < 50; i++) {
         Object lock = new Object();
         synchronized (lock) {
             list.add(lock);
         }
     }
     System.out.println("对象创建完毕");
     sleep1Minutes();
 }, "Thread1");
 thread1.start();
 TimeUnit.SECONDS.sleep(3);
 // 上面创建50个偏向锁,并偏向thread1,就不做打印了,这个如果不信邪可以打印出来看,都是偏向锁
 // 线程2对这30个偏向锁竞争,升级为轻量级锁,并触发重偏向机制
 Thread thread2 = new Thread(() -> {
     for (int i = 0; i < 30; i++) {
         Object lock = list.get(i);
         synchronized (lock) {
             if (i == 18 || i == 19) {
                 // 索引18为第19个锁对象为轻量级锁,索引19为第20个锁对象为重偏向锁,从19以后都是重偏向
                 System.out.println(i + ": " + (ClassLayout.parseInstance(lock).toPrintable()));
             }
         }
     }
     sleep1Minutes();
 }, "Thread2");
 thread2.start();
 ​
 thread2.join();

结果:

 对象创建完毕
 18: java.lang.Object object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           78 f1 5f f1 (01111000 11110001 01011111 11110001) (-245370504)
      
 19: java.lang.Object object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0     4        (object header)                           05 01 a6 c1 (00000101 00000001 10100110 11000001) (-1046085371)
      

这里就只贴出了第一行的状态信息,可以看到在索引18处是轻量级锁,在索引19处是偏向锁。证明重偏向生效。

批量重偏向源码分析

 // InterpreterRuntime::monitorenter
 // 还是从最开始monitorenter分析,进入fast_enter
 if (UseBiasedLocking) {
 ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
 } else {
 ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
 }
 // ======================================================================================
 // ObjectSynchronizer::fast_enter -> synchronizer.cpp[ObjectSynchronizer::fast_enter]
 void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 // 开启了偏向锁
 if (UseBiasedLocking) {
 // 是否在全局安全点,我们上面的代码,肯定不在,所以直接走这个if里面
 if (!SafepointSynchronize::is_at_safepoint()) {
     // 看到这里了吗:revoke_and_rebias 撤销并且重偏向(嘿嘿,找到了第二次加锁偏向锁要撤销了吧)
     BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
     // 注意:这段代码在上面分析偏向锁加锁逻辑没有贴进来,这是重偏向获取锁后,直接退出了。
     if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
         return;
      }
 } 
 // ======================================================================================
  // 下面的逻辑其实就是撤销偏向锁或者重新获取偏向锁   
 // heuristics:这个在上面分析偏向锁撤销没有说明:这个update_heuristics就是更新锁撤销次数的,后面判断是否达到了重偏向阈值就是通过它来判断。这个update_heuristics里面代码主要是这一句 revocation_count = k->atomic_incr_biased_lock_revocation_count();也就是阈值加+1
 // 能够执行到这里,说明上面的代码都不满足,没有返回:1、attempt_rebias=false,只是单纯为的为了撤销偏向锁;2、无锁状态;3、klass支持偏向锁且klass的epoch=obj的epoch
 // 假设达到了阈值20,也就是 update_heuristics = HR_BULK_REVOKE
 HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);
 // 不是偏向锁,直接返回了
 if (heuristics == HR_NOT_BIASED) {
     return NOT_BIASED;
     // 上面分析单个偏向执行撤销,进入这里,很关键,但是这里我们在分析重偏向,
 } else if (heuristics == HR_SINGLE_REVOKE) {
    // 普通偏向锁,单个偏向锁撤销,最终会返回 BIAS_REVOKED
     return BiasedLocking::BIAS_REVOKED;
 }
 //  update_heuristics = HR_BULK_REVOKE 上面都不满足,因此进入此处逻辑
 assert((heuristics == HR_BULK_REVOKE) || (heuristics == HR_BULK_REBIAS), "?");
 // 最重要的,进入批量撤销逻辑:最终会进入 bulk_revoke_or_rebias_at_safepoint 方法
 VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD, (heuristics == HR_BULK_REBIAS),attempt_rebias);
 VMThread::execute(&bulk_revoke);
 return bulk_revoke.status_code();
 // =========================================================================================
 // 偏向锁批量重偏向或者撤销:BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint
 // 先看方法声明:bulk_rebias 等于上面的(heuristics == HR_BULK_REBIAS)条件表达式,肯定为true
 BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o,
                                                                    bool bulk_rebias,
                                                                    bool attempt_rebias_of_object,
                                                                    JavaThread* requesting_thread)
 // 具体逻辑:刚说了bulk_rebias=true,引入进入if (bulk_rebias)逻辑
 if (bulk_rebias) {
     // 修改状态为重偏向核心代码
     int prev_epoch = klass->prototype_header()->bias_epoch();
     // klass的epoch+1
     klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
     int cur_epoch = klass->prototype_header()->bias_epoch();
     // 批量重偏向的核心代码,遍历活动线程,找到偏向锁是当前klass的,修改其epoch值
     for (JavaThread* thr = Threads::first(); thr != NULL; thr = thr->next()) {
         GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
         for (int i = 0; i < cached_monitor_info->length(); i++) {
             MonitorInfo* mon_info = cached_monitor_info->at(i);
             oop owner = mon_info->owner();
             markOop mark = owner->mark();
             if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
                 // We might have encountered this object already in the case of recursive locking
                 assert(mark->bias_epoch() == prev_epoch || mark->bias_epoch() == cur_epoch, "error in bias epoch adjustment");
                 owner->set_mark(mark->set_bias_epoch(cur_epoch));
             }
         }
     }
 }else{
     ....
 }
 // 最后一点逻辑,上面已经完成了批量重偏向修改,已经完成了重偏向,但是是不是忘了啥事,我们是要获取这个锁呀,因此这里就是获取锁后修改状态的核心代码了
 // attempt_rebias_of_object:这个变量,就是尝试重新偏向获取线程,不用怀疑为true,可以看上面的调用链,它是从ObjectSynchronizer::faster_enter进来的,只要不是对象头存了hashCode,都是true。这个hashCode和偏向锁的关系后面单独开一篇分析 
 //  o->mark()->has_bias_pattern():对象头是否是偏向锁模式
 // klass->prototype_header()->has_bias_pattern():类是否是偏向锁模式(还有一种批量撤销后的无锁模式)
 // 很明显,三个条件都为true    
 if (attempt_rebias_of_object &&
     o->mark()->has_bias_pattern() &&
     klass->prototype_header()->has_bias_pattern()) {
     // 重新设置对象头,包括设置偏向线程requesting_thread,新的epoch值为klass的epoch值
     // ❀❀❀❀❀❀❀❀  至此完成重偏向和批量重偏向的所有逻辑
     markOop new_mark = markOopDesc::encode(requesting_thread, o->mark()->age(),
                                            klass->prototype_header()->bias_epoch());
     o->set_mark(new_mark);
     // 修改这次操作的状态
     status_code = BiasedLocking::BIAS_REVOKED_AND_REBIASED;
     if (TraceBiasedLocking) {
         tty->print_cr("  Rebiased object toward thread " INTPTR_FORMAT, (intptr_t) requesting_thread);
     }
 }
 // 最后返回status_code = BiasedLocking::BIAS_REVOKED_AND_REBIASED
 return status_code;、
 // =========================================================================================
 ​
  void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
     // 调用偏向锁撤销和重新获取入口
     BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
     // 返回为BIAS_REVOKED_AND_REBIASED,完成monitorenter获取锁逻辑。✿✿ヽ(°▽°)ノ✿
     if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
         return;
     }
 ​

从上面源码分析可以得出结论,当撤销次数达到了阈值的时候,会执行批量重偏向,并且批量重偏向后也会把当前获取的偏向锁也修改成重偏向模式。获取下一个偏向锁的时候,怎么判断执行重偏向获取呢?

这时候回到偏向锁入口:BiasedLocking::revoke_and_rebias

有这么一段代码

 // klass的epoch和mark的epoch不一致,直接在attempt_rebias情况下,通过原子操作,修改其状态为重偏向,并返回BIAS_REVOKED_AND_REBIASED
 // 这是不是就证明了,当klass的epoch和mark的epoch不一致线程就可以重新获取该偏向锁
 else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
    // The epoch of this biasing has expired indicating that the
    // object is effectively unbiased. Depending on whether we need
    // to rebias or revoke the bias of this object we can do it
    // efficiently enough with a CAS that we shouldn't update the
    // heuristics. This is normally done in the assembly code but we
    // can reach this point due to various points in the runtime
    // needing to revoke biases.
    if (attempt_rebias) {
      assert(THREAD->is_Java_thread(), "");
      markOop biased_value       = mark;
      markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
      markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark);
      if (res_mark == biased_value) {
        return BIAS_REVOKED_AND_REBIASED;
      }
    } else {
      markOop biased_value       = mark;
      markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
      markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
      if (res_mark == biased_value) {
        return BIAS_REVOKED;
      }
    }
  }

看了源码后,提出点问题吧。

修改klass的epoch是否是每次获取一个重偏向锁都会+1,这么做的目的是什么?

答:不会,一个klass只有第一次进入触发重偏向后,才会导致klass->epoch+1,已经被释放了的偏向锁再次重偏向获取的时候,就会比对epoch是否一致,而正在工作的偏向锁,会在第一次执行批量重偏向的时候,将其状态改为和klass->epoch一样,代表已经重偏向了。

如果一致:根据源码可以看到直接返回了BIAS_REVOKED状态,代表已撤销,会走slow_enter逻辑,这也是为什么重偏向只能重偏向一次。

批量撤销

什么是批量撤销

批量撤销指的是批量撤销偏向锁为无锁,它的场景和批量重偏向类似,也是以class为单位操作,当一个class创建的偏向锁对象,一定时间内撤销次数达到了一定阈值的时候,会把线程栈帧中正在使用的偏向锁撤销为无锁状态后晋升为轻量级锁。

为什么要使用批量撤销?

归根结底还是为了解决偏向锁撤销开销大的问题,批量重偏向应用于第二次重偏向(这个概念前面说烂了,不像再解释了)。但是我们现在又有个场景,根据上面重偏向源码的分析,我们发现了,已经重偏向了的偏向锁不能二次重偏向锁,也就是如果再来一个线程对已经重偏向了的锁再次获取,则又会导致一次偏向锁撤销。JVM就认为,你玩儿我呢,一二再,再而三的撤销,当我好欺负,那就都别玩了,直接遍历活动线程将和klass一样的偏向锁撤销成无锁升级到轻量级锁,并对klass的偏向锁标志位无锁状态,后面基于该class生成的对象都是无锁状态。

批量撤销原理

批量撤销原理和批量重偏向原理类似,可以说是一样,上面分析了偏向锁有个计数器,也就是这个方法:update_heuristics里面更新,当计数器阈值达到了执行批量撤销的阈值下,就执行批量撤销。

它们用来计数的计数器都是同一个,那么我们提出个问题,当批量重偏向和批量撤销的阈值相同的情况下,到底执行批量重偏向还是批量撤销呢?ps:下面分析了源码,我们给你答案,乖乖哒

image-20220521123608627.png

批量撤销证明

批量撤销相关参数:

可以通过配置JVM参数 -XX:+PrintFlagsFinal 查看默认启动的相关JVM参数。

  • intx BiasedLockingBulkRebiasThreshold = 20 默认偏向锁批量重偏向阈值
  • intx BiasedLockingBulkRevokeThreshold = 40 默认偏向锁批量撤销阈值

intx BiasedLockingDecayTime = 25000 触发批量撤销偏向锁时间阈值25s(这个参数了解一下,不再使用了,反正我java8已经不能使用了,而且我在源码里面也没有找到这个有关这个参数相关逻辑)

批量撤销测试步骤:

  1. 线程1创建了50个偏向锁对象并偏向线程1。
  2. 线程2在线程1存活并释放了这50个锁的状态下,从第20个锁对象开始触发了重偏向机制。ps:前20个还是锁撤销机制
  3. 线程3继续获取这50个锁里面已经重偏向了的锁(区间20-50),导致锁撤销,达到锁撤销次数40,步骤2已经撤销了20次了,这里可以只需要再撤销20次即可。

代码:

ps:注意这里的代码和上面的代码有点小改动,首先锁对象是基于BiasedLockDemo2类创建的,而不是Object,这是为了提醒在系统使用的时候尽量明确锁目标,如果全用Object很容易造成偏向锁批量撤销,导致后面的锁使用不了偏向锁(这可以作为一个面试题);还有就是打印的时候,对索引+1了,方便查看锁位置信息。

 Thread.sleep(5000);
 List<BiasedLockDemo2> list = new ArrayList<>();
 // 线程1对50个偏向锁加锁,使得偏向锁偏向线程1
 Thread thread1 = new Thread(() -> {
     for (int i = 0; i < 50; i++) {
         BiasedLockDemo2 lock = new BiasedLockDemo2();
         synchronized (lock) {
             list.add(lock);
         }
     }
     System.out.println("对象创建完毕");
     sleep1Minutes();
 }, "Thread1");
 thread1.start();
 TimeUnit.SECONDS.sleep(5);
 ​
 // 线程2,触发重偏向
 Thread thread2 = new Thread(() -> {
     for (int i = 0; i < 50; i++) {
         BiasedLockDemo2 lock = list.get(i);
         synchronized (lock) {
             // >=19的都是偏向锁
             if (i == 18 || i == 19) {
                 System.out.println(Thread.currentThread().getName() + " " + (i + 1) + ": " + (ClassLayout.parseInstance(lock).toPrintable()));
             }
         }
     }
     sleep1Minutes();
 }, "Thread2");
 thread2.start();
 // 等待线程2执行完代码执行完
 TimeUnit.SECONDS.sleep(5);
 ​
 // 触发批量撤销:重偏向撤销0-19(20)+这里撤20-40(20)=40
 Thread thread3 = new Thread(() -> {
     for (int i = 20; i < 40; i++) {
         BiasedLockDemo2 lock = list.get(i);
         synchronized (lock) {
             if (i == 20 || i == 39) {
                 System.out.println(Thread.currentThread().getName() + " " + (i + 1) + ": " + (ClassLayout.parseInstance(lock).toPrintable()));
             }
         }
     }
     sleep1Minutes();
 }, "Thread3");
 thread3.start();
 TimeUnit.SECONDS.sleep(5);
 // 基于类创建的新对象,为无锁状态,synchronized使用的时候,直接升级为轻量级锁
 System.out.println(Thread.currentThread().getName() + " 新建的对象为无锁状态: " + (ClassLayout.parseInstance(new BiasedLockDemo2()).toPrintable()));
 System.out.println(Thread.currentThread().getName() + " 已经重偏向了的锁不会改变状态: " + (ClassLayout.parseInstance(list.get(48)).toPrintable()));
 thread3.join();

结果:

 对象创建完毕
 # 第二次加锁,前20个会造成锁撤销,所以锁对象状态为轻量级锁
 Thread2 19: com.example.demo.blogs.BiasedLockDemo2 object internals:
   0     4        (object header)                           38 f3 af 3a (00111000 11110011 10101111 00111010) (984609592)
 # 第二次加锁,撤销到了20,进入重偏向模式,所以又进入了偏向锁
 Thread2 20: com.example.demo.blogs.BiasedLockDemo2 object internals:
   0     4        (object header)                           0d e1 1b 43 (00001101 11100001 00011011 01000011) (1125900557)
 # 第三次加锁,对第二次重偏向了的锁对象加锁,因为已经重偏向的不能再重偏向,触发锁撤销为轻量级锁
 Thread3 21: com.example.demo.blogs.BiasedLockDemo2 object internals:
  0     4        (object header)                           c8 f0 cf 3a (11001000 11110000 11001111 00111010) (986706120)
 # 同理21,并且到这里,撤销锁次数总计达到了40=第一次加锁撤销的前20+第三次加锁撤销的20重偏向锁
 Thread3 40: com.example.demo.blogs.BiasedLockDemo2 object internals:
   0     4        (object header)                           c8 f0 cf 3a (11001000 11110000 11001111 00111010) (986706120)
 # 基于第三次此加锁,触发了批量撤销,后面新建的对象都是无锁状态,加锁的时候直接使用轻量级锁      
 main 新建的对象为无锁状态: com.example.demo.blogs.BiasedLockDemo2 object internals:
   0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
 # 第二次对锁49进行了重偏向,第三次加锁并没有撤销,因此锁49状态不变
 main 已经重偏向了的锁不会改变状态:  com.example.demo.blogs.BiasedLockDemo2 object internals:
    0     4        (object header)                           05 39 08 6c (00000101 00111001 00001000 01101100) (1812478213)
 ​

偏向锁批量撤销在上面结果里面注释已经描述的很清楚了。

总结一下:当一个类创建的偏向锁撤销次数累计达到了40次以后,就会触发偏向锁批量撤销,后面基于该类创建的所有对象都是无锁状态,不会再使用偏向锁,如果触发了偏向锁批量撤销就得考虑该锁是否是设计有问题,是否需要应用偏向锁场景。

这里也引出了一个可作为面试题的问题,Object这种都在使用的类适合作为锁对象吗?

批量撤销源码分析

如果你认真分析了批量重偏向源码,则再分析批量撤销就轻轻松松

 // InterpreterRuntime::monitorenter
 // 还是从最开始monitorenter分析,进入fast_enter
 if (UseBiasedLocking) {
 ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
 } else {
 ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
 }
 // ======================================================================================
 // ObjectSynchronizer::fast_enter -> synchronizer.cpp[ObjectSynchronizer::fast_enter]
 void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 // 开启了偏向锁
 if (UseBiasedLocking) {
 // 是否在全局安全点,我们上面的代码,肯定不在,所以直接走这个if里面
 if (!SafepointSynchronize::is_at_safepoint()) {
     // 看到这里了吗:revoke_and_rebias 撤销并且重偏向(嘿嘿,找到了第二次加锁偏向锁要撤销了吧)
     BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
     // 注意:这段代码在上面分析偏向锁加锁逻辑没有贴进来,这是重偏向获取锁后,直接退出了。
     if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
         return;
      }
 } 
 // ======================================================================================
 // 从上面已经看到了,要撤销偏向锁,那么为什么是撤销成为了无锁,而不是撤销成偏向状态锁呢?就喜欢这种杠精,我们继续跟源码
 // 找到BiasedLocking::revoke_and_rebias,东西太多,就贴点关键的
 ​
  // 下面的逻辑其实就是撤销偏向锁或者重新获取偏向锁   
 // heuristics:这个在上面分析偏向锁撤销没有说明:这个update_heuristics就是更新锁撤销次数的,后面判断是否达到了重偏向阈值就是通过它来判断。这个update_heuristics里面代码主要是这一句 revocation_count = k->atomic_incr_biased_lock_revocation_count();也就是阈值加+1
 // 能够执行到这里,说明上面的代码都不满足,没有返回:1、attempt_rebias=false,只是单纯为的为了撤销偏向锁;2、无锁状态;3、klass支持偏向锁且klass的epoch=obj的epoch
 // 假设达到了阈值20,也就是 update_heuristics = HR_BULK_REVOKE
 HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);
 // 这次我们吧update_heuristics的核心代码贴出来 
 // **********************************************************update_heuristics
     // 如果撤销数量小于批量撤销阈值,则撤销数量+1;这是不是也可以得出一个结论,一旦达到了批量撤销,这个数字就不会再加了.
     if (revocation_count <= BiasedLockingBulkRevokeThreshold) {
         revocation_count = k->atomic_incr_biased_lock_revocation_count();
     }
     // 如果撤销数量等于了批量撤销阈值,则返回状态为HR_BULK_REVOKE(批量撤销)
     if (revocation_count == BiasedLockingBulkRevokeThreshold) {
         return HR_BULK_REVOKE;
     }
     // 如果撤销数量等于了批量重偏向阈值,则返回状态为HR_BULK_REBIASE(批量重偏向)
     if (revocation_count == BiasedLockingBulkRebiasThreshold) {
         return HR_BULK_REBIAS;
     }
   return HR_SINGLE_REVOKE;    
 // ***********************************************************update_heuristics
 // 不是偏向锁,直接返回了
 if (heuristics == HR_NOT_BIASED) {
     return NOT_BIASED;
     // 上面分析单个偏向执行撤销,进入这里,很关键,但是这里我们在分析重偏向,
 } else if (heuristics == HR_SINGLE_REVOKE) {
    // 普通偏向锁,单个偏向锁撤销,最终会返回 BIAS_REVOKED
     return BiasedLocking::BIAS_REVOKED;
 }
 //  update_heuristics = HR_BULK_REVOKE 上面都不满足,因此进入此处逻辑
 assert((heuristics == HR_BULK_REVOKE) || (heuristics == HR_BULK_REBIAS), "?");
 // 最重要的,进入批量撤销逻辑:最终会进入 bulk_revoke_or_rebias_at_safepoint 方法
 VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD, (heuristics == HR_BULK_REBIAS),attempt_rebias);
 VMThread::execute(&bulk_revoke);
 return bulk_revoke.status_code();
 // =========================================================================================
 // 偏向锁批量重偏向或者撤销:BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint
 // 先看方法声明:bulk_rebias变量 此时和批量重偏向的逻辑就不同了,因为达到了批量撤销,返回了HR_BULK_REVOKE,因此上面的(heuristics == HR_BULK_REBIAS)条件表达式,就为false了
 BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o,
                                                                    bool bulk_rebias,
                                                                    bool attempt_rebias_of_object,
                                                                    JavaThread* requesting_thread)
 // 具体逻辑:刚说了bulk_rebias=true,引入进入if (bulk_rebias)逻辑
 if (bulk_rebias) {
    // 批量重偏向逻辑,上面已经分析了
 }else{
    // 批量撤销核心代码
    // Disable biased locking for this data type. Not only will this
     // cause future instances to not be biased, but existing biased
     // instances will notice that this implicitly caused their biases
     // to be revoked.
     // 设置klass为无锁状态,也就是禁用偏向
     klass->set_prototype_header(markOopDesc::prototype());
 ​
     // 批量撤销核心代码:遍历活动线程,找到klass相同的偏向锁,进行撤销
     for (JavaThread* thr = Threads::first(); thr != NULL; thr = thr->next()) {
       GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
       for (int i = 0; i < cached_monitor_info->length(); i++) {
         MonitorInfo* mon_info = cached_monitor_info->at(i);
         oop owner = mon_info->owner();
         markOop mark = owner->mark();
         if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
           // 撤销正在使用的偏向锁核心代码,注意第第3个参数,这里是撤销偏向锁核心逻辑
           revoke_bias(owner, false, true, requesting_thread);
         }
       }
     }
     // 撤销当前即将获取的偏向锁,注意这里的第一个参数是不一样的
     revoke_bias(o, false, true, requesting_thread);
 }
 // 撤销锁逻辑这么重要怎么能不分析呢,下面看:revoke_bias
 // ************************************************ revoke_bias
 // 重要的是第二个参数,allow_rebias允许重偏向,这里两次调用都是为false,说明撤销后不能再重偏向了,也就是说彻底告别了偏向锁
 static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias, bool is_bulk, JavaThread* requesting_thread)
 // 这里的代码逻辑细看挺多的,但是跟我们关联的就只有allow_rebias这个参数,发现只要它为false,不管走什么逻辑都会给设置成obj->set_mark(unbiased_prototype);不可重偏向状态。
 // ************************************************ revoke_bias
 ​
   BiasedLocking::Condition status_code = BiasedLocking::BIAS_REVOKED;
 // attempt_rebias_of_object:这个变量,就是尝试重新偏向获取线程,不用怀疑为true,可以看上面的调用链,它是从ObjectSynchronizer::faster_enter进来的,只要不是对象头存了hashCode,都是true。这个hashCode和偏向锁的关系后面单独开一篇分析 
 //  o->mark()->has_bias_pattern():对象头是否是偏向锁模式 
 // klass->prototype_header()->has_bias_pattern():类是否是偏向锁模式(还有一种批量撤销后的无锁模式)  注意,在上面撤销了,这里已经为false了,因此下面代码就不执行了
 // 很明显,三个条件满足不完
 if (attempt_rebias_of_object &&
     o->mark()->has_bias_pattern() &&
     klass->prototype_header()->has_bias_pattern()) {
 }
 // 最后返回status_code = BiasedLocking::BIAS_REVOKED;默认值
 return status_code;、
 // =========================================================================================
 ​
  void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
     // 调用偏向锁撤销和重新获取入口
     BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
     // 返回为cond=BIAS_REVOKED,不等于BIAS_REVOKED_AND_REBIASED,执行slow_enter逻辑,也就是轻量级锁吧,先这样理解着。
     if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
         return;
     }
 ​

批量撤销的源码也看完了,回答一下上面的问题:

它们用来计数的计数器都是同一个,那么我们提出个问题,当批量重偏向和批量撤销的阈值相同的情况下,到底执行批量重偏向还是批量撤销呢?

答,当批量重偏向和批量撤销阈值相同的情况下,走批量撤销逻辑,主要判断依据在于update_heuristics的判断批量撤销在批量重偏向之前,最后调用批量重偏向或撤销方法(bulk_revoke_or_rebias_at_safepoint)的表达式(heuristics == HR_BULK_REBIAS)为false,因此在批量重偏向和批量撤销阈值相等的情况下,批量撤销优先级高于批量重偏向。

 // **********************************************************update_heuristics
  // 如果撤销数量小于批量撤销阈值,则撤销数量+1;这是不是也可以得出一个结论,一旦达到了批量撤销,这个数字就不会再加了.
  if (revocation_count <= BiasedLockingBulkRevokeThreshold) {
      revocation_count = k->atomic_incr_biased_lock_revocation_count();
  }
     // 如果撤销数量等于了批量撤销阈值,则返回状态为HR_BULK_REVOKE(批量撤销)
  if (revocation_count == BiasedLockingBulkRevokeThreshold) {
      return HR_BULK_REVOKE;
  }
     // 如果撤销数量等于了批量重偏向阈值,则返回状态为HR_BULK_REBIASE(批量重偏向)
  if (revocation_count == BiasedLockingBulkRebiasThreshold) {
      return HR_BULK_REBIAS;
  }
 return HR_SINGLE_REVOKE;    
 // ***********************************************************update_heuristics

总结

至此已经对偏向锁和无锁基本上完全分析了,比较难的在于批量重偏向和批量撤销这一块,如果能看到这里的人应该很少,就当自己分析的笔记吧。

偏向锁适用于单线程获取锁,并且不会主动释放锁,在持有锁期间可能会因为线程竞争或者批量撤销导致撤销偏向锁,升级为轻量级锁,这一步是竞争线程在全局安全点执行的。如果偏向锁设计不当,会造成批量重偏向和批量撤销,这两种工作模式都是为了减少偏向锁撤销次数的优化,主要通过偏向锁的klass撤销次数来判断是否进入这两种模式,最后以klass的epoch和对象mark的epoch相比较,从而达到判断偏向锁是否可重偏向的条件。

上面一直在说,为了优化偏向锁撤销,因为偏向锁撤销会导致等待全局安全点(safe point),那么什么是全局安全点,还是大致说下。

全局安全点是指在这个时刻JVM能够安全、可控的操作对象,不会有其它工作线程来操作,因为JVM本质也是一个应用也是一个线程,说白了就是现在只能我来操作,你基于JVM的应用,别来跟我抢。要实现这个有两种方式:

一、JVM是老大,告诉所有上层应用都不准跟我抢,给我暂停了,这对于上层应用来说是被动暂停,有的线程人家本来就不会操作到JVM即将操作的区域,也给人家停了,就像另可杀错不放过的道理一样,太霸道,不太好

二、JVM主动设置一个全局变量,标识这个区域我在使用,其它线程看到这个标识就等着,类似此路正在维修告示牌一样,这也是称为主动暂停;一般都是采用这种方式

最后偏向锁总体加锁流程图:

image-20220521161326565.png

扩展

上面分析了批量重偏向和批量撤销,基本上都快把偏向锁的核心源码都走了一遍了,但是发现关键点没有,他娘的我简单第二次获取偏向锁的源码呢,就是该偏向锁已经偏向了当前线程,当前线程要再次获取这个偏向锁的源码呢,这才是偏向锁场景执行最多最核心的的源码吧。但是我们发现biasedLocking.cpp文件里面完全没有。还有偏向锁释放的时候,我也是在分析轻量级锁源码的时候,释放偏向锁synchronizer.cpp里面fast_exit第一行断言就是,当前锁状态不是偏向锁,我就在想,不是synchronized由monitorenter和monitorexit成对出现吗,为啥monitorexit的核心逻辑第一行就是不能是偏向锁。最后查阅各种资料才发现,monitorenter指令和monitorexit指令不简单的直接对应了interpreterRuntime.cpp的monitorenter和monitorext它还有前面的逻辑。

主要参考这一篇博客:www.jianshu.com/p/4758852cb…

简单来说JVM执行字节码有两种方式,一种是字节码解释器(bytecodeInterpreter),用C++实现了每条JVM指令(如monitorenterinvokevirtual等),其优点是实现相对简单且容易理解,缺点是执行慢。后者是模板解释器(templateInterpreter),其对每个指令都写了一段对应的汇编代码。因此,其实我们上面分析的interpreterRuntime.cpp文件的monitorenter还不是最开始的入口,引入汇编比较难以理解,因此主要查看bytecodeInterpreter文件。

因为我是下载的openJDK8的源码,因此和参考博客中的有些代码不一样,但是大致还是可以分析一下

monitorenter:

 // 引用博客:https://www.jianshu.com/p/4758852cbff4
 // code 5:如果偏向的线程是自己且epoch等于class的epoch
 if  (anticipated_bias_locking_value == 0) {
     // already biased towards this thread, nothing to do
     if (PrintBiasedLockingStatistics) {
         (* BiasedLocking::biased_lock_entry_count_addr())++;
     }
     success = true;
 }

monitorext:

 // 引用博客:https://www.jianshu.com/p/4758852cbff4
 CASE(_monitorexit): {
   oop lockee = STACK_OBJECT(-1);
   CHECK_NULL(lockee);
   // derefing's lockee ought to provoke implicit null check
   // find our monitor slot
   BasicObjectLock* limit = istate->monitor_base();
   BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
   // 从低往高遍历栈的Lock Record
   while (most_recent != limit ) {
     // 如果Lock Record关联的是该锁对象
     if ((most_recent)->obj() == lockee) {
       BasicLock* lock = most_recent->lock();
       markOop header = lock->displaced_header();
       // 释放Lock Record
       most_recent->set_obj(NULL);
       // 如果是偏向模式,仅仅释放Lock Record就好了。否则要走轻量级锁or重量级锁的释放流程
       if (!lockee->mark()->has_bias_pattern()) {
         bool call_vm = UseHeavyMonitors;
         // header!=NULL说明不是重入,则需要将Displaced Mark Word CAS到对象头的Mark Word
         if (header != NULL || call_vm) {
           if (call_vm || Atomic::cmpxchg_ptr(header, lockee->mark_addr(), lock) != lock) {
             // CAS失败或者是重量级锁则会走到这里,先将obj还原,然后调用monitorexit方法
             most_recent->set_obj(lockee);
             CALL_VM(InterpreterRuntime::monitorexit(THREAD, most_recent), handle_exception);
           }
         }
       }
       //执行下一条命令
       UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
     }
     //处理下一条Lock Record
     most_recent++;
   }
   // Need to throw illegal monitor state exception
   CALL_VM(InterpreterRuntime::throw_illegal_monitor_state_exception(THREAD), handle_exception);
   ShouldNotReachHere();
 }

经过分析,发现并不是字节码monitorenter和monitorexti就直接对应了interpreterRuntime.cpp的monitorenter和monitorext逻辑,在interpreterRuntime.cpp中monitorenter偏向锁入口更多的是批量重偏向以及批量撤销。

\