08.从源码揭秘偏向锁的升级

简介: 大家好,我是王有志。上一篇学习了synchronized的用法,今天我们深到synchronized的原理,来学习偏向锁升级到轻量级锁的过程。

大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。快来加入我们的Java提桶跑路群:共同富裕的Java人

今天开始,我会和大家一起深入学习synchronized的原理,原理部分会涉及到两篇:

  • 偏向锁升级到轻量级锁的过程
  • 轻量级锁升级到重量级锁的过程

今天我们先来学习偏向锁升级到轻量级锁的过程。因为涉及到大量HotSpot源码,会有单独的一篇注释版源码的文章。

通过本篇文章,你们解答synchronized都问啥?中统计到的如下问题:

  • 详细描述下synchronized的实现原理(67%)
  • 为什么说synchronized是可重入锁?(67%)
  • 详细描述下synchronized的锁升级(膨胀)过程(67%)
  • 偏向锁是什么?synchronized是怎样实现偏向锁的?(100%)
  • Java 8之后,synchronized做了哪些优化?(50%)

准备工作

正式开始分析synchronized源码前,我们先做一些准备:

  • HotSpot源码准备:Open JDK 11
  • 字节码工具,推荐jclasslib插件
  • 用于跟踪对象状态的jol-core包。

Tips

  • 可以使用javap命令和IDEA自带的字节码工具;
  • jclasslib的优势在于可以直接跳转到相关命令的官方站点。

示例代码

准备一个简单的示例代码:

public class SynchronizedPrinciple {

  private int count = 0;

  private void add() {
    synchronized (this) {
      count++;
    }
  }
}

通过工具,我们可以得到如下字节码:

aload_0
dup
astore_1
monitorenter // 1
aload_0
dup
getfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
iconst_1
iadd
putfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
aload_1
monitorexit // 2
goto 24 (+8)
astore_2
aload_1
monitorexit // 3
aload_2
athrow
return

synchronized修饰代码块,编译成了两条指令:

我们注意到,monitorexit出现了两次。注释2的部分是程序执行正常,注释3的部分是程序执行异常。Java团队连程序异常的情况都替你考虑到了,他真的,我哭死。

Tips

  • 使用synchronized修饰代码块作为示例的原因是,修饰方法时仅在access_flag设置ACC_SYNCHRONIZED标志,并不直观;
  • Java并不是只能通过monitorexit退出监视器, Java曾在Unsafe类中提供过进出监视器的方法。
Unsafe.getUnsafe.monitorEnter(obj);
Unsafe.getUnsafe.monitorExit(obj);

Java 8可以使用,Java 11已经移除,具体移除的版本我就不太清楚了。

jol使用示例

可以通过jol-core来跟踪对象状态。Maven依赖:

<dependency>  
  <groupId>org.openjdk.jol</groupId>  
  <artifactId>jol-core</artifactId>  
  <version>0.16</version>  
</dependency>

使用示例:

public static void main(String[] args) {
  Object obj = new Object();
  System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

从monitorenter处开始

在HotSpot中,monitorenter指令对应这两大类解析方式:

由于bytecodeInterpreter基本退出了历史舞台,我们以模板解释器X86实现templateTable_x86为例。

Tips

monitorenter的执行方法是templateTable_x86#monitorenter,该方法中,我们只需要关注4438行执行的__ lock_object(rmon),调用了interp_masm_x86#lock_object方法:

void InterpreterMacroAssembler::lock_object(Register lock_reg) {
  if (UseHeavyMonitors) {// 1
    // 重量级锁逻辑
    call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),  lock_reg);  
} else {
  Label done;
  Label slow_case;
  if (UseBiasedLocking) {// 2
    // 偏向锁逻辑
    biased_locking_enter(lock_reg, obj_reg, swap_reg, tmp_reg, false, done, &slow_case);
  }

  // 3
  bind(slow_case);
  call_VM(noreg,   CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),  lock_reg);
    bind(done);
  ......
}

注释1和注释2的部分,是两个JVM参数:

// 启用重量级锁
-XX:+UseHeavyMonitors
// 启用偏向锁
-XX:+UseBiasedLocking

注释1和注释3,调用InterpreterRuntime::monitorenter方法,注释1是直接使用重量级锁的配置,那么可以猜到,注释3是获取偏向锁失败锁升级为重量级锁的逻辑。

对象头(markOop)

正式开始前,先来了解对象头(markOop)。实际上,markOop的注释已经揭露了它的“秘密“:

The markOop describes the header of an object.
.....
Bit-format of an object header (most significant first, big endian layout below):
64 bits:
unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
JavaThread:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
......
[JavaThread
| epoch | age | 1 | 01] lock is biased toward given thread
[0 | epoch | age | 1 | 01] lock is anonymously biase

注释详细的描述了64位大端模式下Java对象头的结构:

图1:对象头.png

Tips

对象头中的大部分结构都很容易理解,但epoch是什么?

注释中将epoch描述为“used in support of biased locking”。OpenJDK wiki中Synchronization是这样描述epoch的:

An epoch value in the class acts as a timestamp that indicates the validity of the bias.

epoch类似于时间戳,表示偏向锁的有效性。它的在批量重偏向阶段(biasedLocking#bulk_revoke_or_rebias_at_safepoint)更新:

static BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o, bool bulk_rebias, bool attempt_rebias_of_object, JavaThread* requesting_thread) {
  {
    if (bulk_rebias) {
      if (klass->prototype_header()->has_bias_pattern()) {
        klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
      }
    }
  }
}

JVM通过epoch来判断是否适合偏向锁,超过阈值后JVM会升级偏向锁。JVM提供了参数来调节这个阈值。

// 批量重偏向阈值
-XX:BiasedLockingBulkRebiasThreshold=20
// 批量撤销阈值
-XX:BiasedLockingBulkRevokeThreshold=40

Tips:更新的是klassepoch

偏向锁(biasedLocking)

系统开启了偏向锁,会进入macroAssembler_x86#biased_locking_enter方法。该方法首先是获取对象的markOop

Address mark_addr         (obj_reg, oopDesc::mark_offset_in_bytes());
Address saved_mark_addr(lock_reg, 0);

我将接下来的流程分为5个分支,按照执行顺序和大家一起分析偏向锁的实现逻辑。

Tips

  • 了解偏向锁流程即可,因此以图示为主,源码分析放在偏向锁源码分析中;

  • 偏向锁源码分析以注释为主,详细标注了每个分支;

  • 这部分实际上包含了撤销重偏向两个跳转标签,分支图示中有说明;

  • 源码使用位掩码技术,为了便于区分,二进制数字用0B开头,并补齐4位。

分支1:是否可偏向?

偏向锁的前置条件,逻辑非常简单,判断当前对象markOop的锁标志,如果已经升级,执行升级流程;否则继续向下执行。

图2:是否可偏向.png

Tips:虚线部分逻辑位于其它类中。

分支2:是否重入偏向?

目前JVM已知markOop的锁标志位为0B0101,处于可偏向状态,但不清楚是已经偏向还是尚未偏向。HotSopt中使用anonymously形容可偏向但尚未偏向某个线程的状态,称这种状态为匿名偏向。此时对象头如下:

图3:匿名偏向对象头.png

此时要做的事情就比较简单了,判断是否为当前线程重入偏向锁。如果是重入,直接退出即可;否则继续向下执行。

图4:是否重入偏向锁?.png

Tips:今天刷到一个帖子,Javaer和C++er争论可重入锁和递归锁,有兴趣的可以看一文看懂并发编程中的锁我简单解释了可重入锁和递归锁的关系。

分支3:是否依旧可偏向?

注释描述了不是重入偏向锁的情况:

At this point we know that the header has the bias pattern and that we are not the bias owner in the current epoch. We need to figure out more details about the state of the header in order to know what operations can be legally performed on the object's header.

此时可能存在两种情况:

  • 不存在竞争,重新偏向某个线程;
  • 存在竞争,尝试撤销。

图5:是否依旧可偏向.png

偏向锁撤销的部分稍微复杂,使用对象klassmarkOop替换对象的markOop,关键技术是CAS

分支4:epoch是否过期?

目前偏向锁的状态是可偏向,且偏向其他线程。此时的逻辑只需要片段epoch是否有效即可。

图6:epoch是否过期?.png

重新偏向的可以用一句话描述,构建markOop进行CAS替换。

分支5:重新偏向

目前偏向锁的状态是,可偏向,偏向其它线程,epoch未过期。此时要做的是在markOop中设置当前线程,也就是偏向锁重新偏向的过程,和分支4的部分非常相似。

撤销和重偏向

获取偏向锁失败后,执行InterpreterRuntime::monitorenter方法,位于interpreterRuntime中:

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  if (UseBiasedLocking) {
    // 完整的锁升级路径
    // 偏向锁->轻量级锁->重量级锁
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    // 跳过偏向锁的锁升级路径
    // 轻量级锁->重量级锁
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
IRT_END

ObjectSynchronizer::fast_enter位于synchronizer.cpp#fast_enter

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
  if (UseBiasedLocking) {
    if (!SafepointSynchronize::is_at_safepoint()) {
      // 撤销和重偏向
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj,  attempt_rebias, THREAD);
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    } else {
      BiasedLocking::revoke_at_safepoint(obj);
    }
  }
  // 跳过偏向锁
  slow_enter(obj, lock, THREAD);
}

BiasedLocking::revoke_and_rebias的精简注释版放在了偏向锁源码分析的第2部分。

轻量级锁(basicLock)

如果获取偏向锁失败,此时会执行ObjectSynchronizer::slow_enter,该方法位于synchronizer#slow_enter

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  // 无锁状态 ,获取偏向锁失败后有撤销逻辑,此时变为无锁状态
  if (mark->is_neutral()) {
    // 将对象的markOop复制到displaced_header(Displaced Mark Word)上
    lock->set_displaced_header(mark);
    // CAS将对象markOop中替换为指向锁记录的指针
    if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
      // 替换成功,则获取轻量级锁
      TEVENT(slow_enter: release stacklock);
      return;
    }
  } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {  
    //  重入情况
    lock->set_displaced_header(NULL);
    return;
  }
  // 重置displaced_header(Displaced Mark Word)
  lock->set_displaced_header(markOopDesc::unused_mark());
  // 锁膨胀
  ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);  
}

直接引用《Java并发编程的艺术》中关于轻量级锁加锁的过程:

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁的逻辑非常简单,使用到的关键技术也是CAS。此时markOop的结构如下:

图7:轻量级锁对象头.png

在monitorexit处结束

处于偏向锁或者轻量级锁时,monitorexit的逻辑非常简单。有了monitorenter的经验,我们很容易分析到monitorexit的调用逻辑:

  1. templateTable_x86#monitorexit
  2. interp_masm_x86#un_lock
  3. 锁的退出逻辑
  4. 偏向锁:macroAssembler_x86#biased_locking_exit
  5. 轻量级锁:interpreterRuntime#monitorexit
  6. ObjectSynchronizer#slow_exit
  7. ObjectSynchronizer#fast_exit

代码就留给大家自行探索了,在这里给出我的理解。

通常,我会简单的认为偏向锁退出时,什么都不需要做(即偏向锁不会主动释放);而对于轻量级锁来说,至少需要经历两个步骤:

  • 重置displaced_header
  • 释放锁记录

因此,从退出逻辑上来说,轻量级锁的性能是稍逊于偏向锁的。

总结

我们对这一阶段的内容做个简单的总结,偏向锁和轻量级锁的逻辑并不复杂,尤其是轻量级锁。

偏向锁和轻量级锁的关键技术都是CAS,当CAS竞争失败,说明有其它线程尝试抢夺,从而导致锁升级。

偏向锁在对象markOop中记录第一次持有它的线程,当该线程不断持有偏向锁时,只需要简单的比对即可,适合绝大部分场景是单线程执行,但偶尔可能会存在线程竞争的场景。

但问题是,如果线程交替持有执行,偏向锁的撤销和重偏向逻辑复杂,性能差。因此引入了轻量级锁,用来保证交替进行这种“轻微”竞争情况的安全。

另外,关于偏向锁的争议比较多,主要在两点:

  • 偏向锁的撤销对性能影响较大;
  • 大量并发时,偏向锁非常鸡肋。

实际上,Java 15中已经放弃了偏向锁(JEP 374: Deprecate and Disable Biased Locking),但由于大部分应用还跑在Java 8上,我们还是要了解偏向锁的逻辑。

最后再辟个谣(或者是被打脸?),轻量级锁中并没有任何自旋的逻辑

Tips:好像漏掉了批量撤销和批量重偏向~~


好了,今天就到这里了,Bye~~

目录
相关文章
|
5月前
|
存储 安全 Java
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
37 0
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
|
10月前
|
存储 Java
一文打通锁升级(偏向锁,轻量级锁,重量级锁)
一文打通锁升级(偏向锁,轻量级锁,重量级锁)
|
10月前
|
安全 Java
锁升级原理
锁升级是指在多线程环境下,当一个线程持有了低级别的锁(如偏向锁或轻量级锁)时,如果有其他线程也要获取这个锁,那么就需要将锁升级为重量级锁。这样可以保证在并发情况下,多个线程之间的互斥访问。
162 1
|
10月前
|
存储 Java
重量级锁,偏向锁和轻量级锁
重量级锁,偏向锁和轻量级锁
76 0
|
11月前
|
存储 Java
sychronized 锁升级
sychronized 锁升级
59 0
|
缓存 Java 数据库
synchronized锁升级的过程
之前只是了解过一些悲观锁的底层原理,和他具体是如何锁住线程的一些细节,正好今天休息,结合一些文章和自己的实践操作,整理成了一篇关于synchronized锁升级的过程,希望能对大家有所帮助.
149 0
synchronized锁升级的过程
|
安全 Java 程序员
|
安全 Java 调度
【Java 并发编程】线程锁机制 ( 锁的四种状态 | 无锁状态 | 偏向锁 | 轻量级锁 | 重量级锁 | 锁竞争 | 锁升级 )
【Java 并发编程】线程锁机制 ( 锁的四种状态 | 无锁状态 | 偏向锁 | 轻量级锁 | 重量级锁 | 锁竞争 | 锁升级 )
222 0
【Java 并发编程】线程锁机制 ( 锁的四种状态 | 无锁状态 | 偏向锁 | 轻量级锁 | 重量级锁 | 锁竞争 | 锁升级 )