Skip to content

JUC synchronized锁升级

本文主要介绍synchronized锁升级相关知识,包括Monitor、偏向锁、轻量级锁和重量级锁。

1. 对象头

1.1 对象头概述

在Java中,每个对象不仅包括实例数据,还包括对象头,其中存储着一些关于对象的信息。

以32位虚拟机为例,普通对象对象头如下,包含Mark WordKlass Word

txt
|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

数组对象的对象头如下,除了Mark WordKlass Word,还包括array length

txt
|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

1.2 对象头的组成

1.2.1 Mark Word

这部分主要用来存储对象自身的运行时数据,如hashcodegc分代年龄等。Mark Word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark Word为32位,64位JVM为64位。 为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

txt
|----------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                     |       State        |
|----------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1(0) | lock:2 |       Normal       |
|----------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1(1) | lock:2 |       Biased       |
|----------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30             | lock:2 | Lightweight Locked |
|----------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30     | lock:2 | Heavyweight Locked |
|----------------------------------------------------------|--------------------|
|                                                 | lock:2 |    Marked for GC   |
|----------------------------------------------------------|--------------------|

其中各部分的含义如下:

lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。

biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

biased_locklock状态
001无锁(正常)
101偏向锁
000轻量级锁
010重量级锁
011GC标记

age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。

thread:持有偏向锁的线程ID。

epoch:偏向时间戳。

ptr_to_lock_record:指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:指向管程Monitor的指针。

64位下的标记字与32位的相似,不再赘述。

1.2.2 Klass Word

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM长度为32位,64位的JVM长度为64位。

如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer,即普通对象指针。开启该选项后,下列指针将压缩至32位:

  1. 每个Class的属性指针(即静态变量)
  2. 每个对象的属性指针(即对象变量)
  3. 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGenClass对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

1.2.3 array length

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

2. Monitor对象

2.1 Monitor原理

Monitor,又称为监视器或管程

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象加锁(重量级锁)之后,该对象的Mark Word中就有指向Monitor对象的指针。

现有多个线程对同一个对象加锁,即synchronized(obj)

  • 当有线程(Thread-2)给对象加锁时,该对象的Mark Word(此时对象头状态为10,重量级锁)指向Monitor对象;
  • 把Monitor中的Owner指向该线程(Thread-2);
  • 如果有其他线程(Thread-3、Thread-4、Thread-5)试图给对象加锁,则会被阻塞,进入EntryList中等待;
  • 如果获得锁的线程(Thread-2)已执行完,则放弃锁,唤醒被阻塞(BLOCKED)的线程来竞争锁;
  • WaitSet中的线程(Thread-0、Thread-1)是之前获得过锁,但条件不满足放弃锁,进入WAITING/TIMED WAITING状态的线程。

注意:

  • 如果只是普通对象,即不是锁对象,则不会关联Monitor对象,不遵守上述规则;
  • 只有多线程竞争同一把锁时才会出现上述情况;

monitor

2.2 Monitor字节码分析

现有如下代码:

java
static final Object lock = new Object();
private static int count = 0;

public static void main(String[] args) {
    synchronized (lock){
        count++;
    }
}

使用javap -p -v xxx命令反编译字节码会产生如下指令:

txt
public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: (0x0009) ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=3, args_size=1
       0: getstatic     #2                  // Field lock:Ljava/lang/Object;
       3: dup
       4: astore_1
       5: monitorenter
       6: getstatic     #3                  // Field count:I
       9: iconst_1
      10: iadd
      11: putstatic     #3                  // Field count:I
      14: aload_1
      15: monitorexit
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit
      22: aload_2
      23: athrow
      24: return
    Exception table:
       from    to  target type
           6    16    19   any
          19    22    19   any

可以看到,在Java代码中写的synchronized关键字,会在字节码层面生成monitorentermonitorexit指令:

  • 在第5行指令执行了monitorenter指令,该指令会使线程尝试获取锁对象(lock对象)关联的Monitor对象,即将Monitor对象的Owner字段指向当前线程,如果获取失败,则会导致当前线程进入阻塞状态,即进入Monitor对象的EntryList字段中;
  • 在第15行指令执行了monitorexit指令,该指令会使线程正常退出同步代码块,即当前线程会释放Monitor对象的所有权,并且唤醒阻塞队列中的线程,让他们竞争获取锁;
  • 在第21行指令同样执行了monitorexit指令,该指令是为了异常发生导致退出时,线程同样也可以释放锁。在下面的异常表中,我们可以看到有两条记录:第一条记录是指如果第6行到第16行指令发生异常,则跳转到第19行指令处理异常(第19行就是异常处理指令,保证释放锁);第二条记录是指如果第19行到第22行指令发生异常,则跳转到第19行指令处理异常;

综上,当我们使用synchronized时,Java为我们添加了monitorentermonitorexit指令,使得我们可以正常获取锁和释放锁,同时,也自动添加了异常处理机制,使得在异常发生时,也可以释放锁。

除了使用monitorentermonitorexit指令,Java编译器还为我们添加了临时的局部变量槽,在下面的信息中,locals=3表示局部变量表有三个槽位:

txt
stack=2, locals=3, args_size=1

但是在代码中只明确声明了一个参数 args,并没有其他局部变量,这看起来有点不合理。

原因在于 Java 编译器在处理 synchronized 语句时,会生成额外的字节码,这些字节码需要使用局部变量槽来存储临时信息。

在给定的 main 方法代码中,局部变量的最大槽数是 3。这可能初看起来有点反直观,因为代码中只明确声明了一个参数 args,并没有其他局部变量。

原因在于 Java 编译器在处理 synchronized 语句时,会生成额外的字节码,这些字节码需要使用局部变量槽来存储临时信息。

每个槽位存储的内容具体来说如下:

  1. String[] args 参数: 这是 main 方法的参数,它占用一个局部变量槽(通常是槽位 0)。
  2. synchronized (lock) 块: synchronized 语句需要获取并最终释放给定对象的监视器(monitor)。为了正确处理正常退出和异常退出(确保在异常发生时也能释放锁),编译器会生成包含 monitorentermonitorexit 指令的字节码。在这个过程中,需要一个临时局部变量槽来存储被锁定的对象引用 (lock),以便在 monitorexit 指令中使用,尤其是在异常处理路径中。这个临时变量通常占用一个槽(槽1,如上面的astore_1指令)。
  3. 异常处理的临时变量: synchronized 块的实现隐含地包含了一个 try-finally 结构,或者更准确地说,一个通过异常表实现的等价结构。当 synchronized 块内的代码抛出异常时,控制流会跳转到异常处理代码,这段代码会执行 monitorexit 来释放锁,然后重新抛出捕获到的异常。为了临时持有这个被捕获的异常对象以便重新抛出,编译器会使用另一个临时局部变量槽(槽2,如上面的astore_2指令)。

因此,尽管在源代码中只看到了 args,但编译器为了实现 synchronized 块的语义(特别是其健壮的异常处理),会使用额外的两个局部变量槽来存储临时的对象引用和异常对象。

总结起来,这 3 个槽分别是:

  • 槽 0: args 参数
  • 槽 1: 存储 lock 对象引用的临时变量(由 synchronized 生成)
  • 槽 2: 存储捕获到的异常对象的临时变量(由 synchronized 的异常处理生成)

3. synchronized锁升级

3.1 重量级锁

重量级锁,就是利用Monitor对象来决定锁对象的拥有者,此时对象头的状态为10

3.2 轻量级锁

3.2.1 概念

在 Java 的 HotSpot JVM 中(JDK 1.6),轻量级锁(Lightweight Locking)是 synchronized 关键字实现的一种锁优化技术。它是为了在没有实际锁竞争的情况下,减少传统重量级锁(使用操作系统互斥量)带来的性能开销而引入的。

轻量级锁假设大多数锁在整个同步周期内都没有竞争,即线程1持有锁对象期间,没有其他线程竞争锁对象。它试图在用户空间(User Space)而非内核空间(Kernel Space)完成同步操作,从而避免操作系统线程上下文切换的昂贵开销。

轻量级锁主要依赖于两个概念:

  1. 锁记录(Lock Record): 每个线程在自己的栈帧中会分配一块区域,用作锁记录。当线程尝试获取轻量级锁时,就会在自己的栈上创建这样一个锁记录。
  2. 对象头的 Mark Word: 每个对象都有一个对象头,其中的 Mark Word 用于存储对象的运行时数据,包括锁状态信息。

轻量级锁对使用者是透明的,即我们仍然使用synchronized关键字。

3.2.2 锁的获取

假设有如下代码:

java
static final Object obj = new Object();
public static void method1(){
    synchronized (obj){
        // 同步代码块
    }
}

当线程0执行method1()方法时,会获取轻量级锁,过程如下:

  1. 创建锁记录: 当线程0尝试获取一个对象 obj 的锁时,如果需要创建轻量级锁,JVM 会在线程0的栈帧中创建一个锁记录。

    image-20250502144421814

  2. 复制对象头: JVM 会将对象 obj 当前的 Mark Word 复制到线程0的锁记录的 displaced_header 字段中。

    image-20250502144656967

  3. CAS 尝试修改对象头: 线程 0 使用 CAS (Compare-and-Swap) 操作尝试将对象 obj 的 Mark Word 替换为指向线程 0 栈中这个锁记录的指针

    • 如果 CAS 成功: 线程 0 成功获取轻量级锁。此时,对象 obj 的 Mark Word 指向线程 0 的栈中的锁记录,锁状态变为轻量级锁。

      image-20250502144805678

    • 如果 CAS 失败: 这通常意味着发生了锁竞争(有其他线程也在尝试获取同一个锁)。轻量级锁就会**膨胀(升级)**为重量级锁。

3.2.3 锁的释放

当持有轻量级锁的线程退出同步块时,它会尝试使用 CAS 操作将对象 obj 的 Mark Word 恢复为锁记录(Lock Record)中保存的原始 Mark Word 值。

  • 如果 CAS 成功: 说明在持有锁期间没有竞争发生,锁被成功释放,对象头恢复到之前的状态(如无锁)。
  • 如果 CAS 失败: 说明在持有锁期间发生了竞争,锁已经膨胀为重量级锁。线程会按照重量级锁的方式来释放锁。

3.2.4 锁膨胀

如果某个线程(Thread-1)在为对象加轻量级锁的过程中,CAS操作无法成功,有一种情况是有其他线程已经为该对象加上了轻量级锁,这是需要进行锁膨胀,将轻量级锁变为重量级锁。

  • 当Thread-1进行轻量级加锁时,Thread-0已经为该对象加上了轻量级锁:

    image-20250502145519992

  • 这是Thread-1加轻量级锁失败,进入锁膨胀流程:

    • 为obj对象申请Monitor锁,让obj对象头状态变为重量级锁(10),并且指向Monitor对象;
    • 自己(Thread-1)进入Monitor的EntryList队列进行阻塞;

    image-20250502145733193

  • 当Thread-0退出同步代码块解锁时,使用CAS将Mark Word的值恢复给对象头,但是操作失败,这时会进入重量级解锁流程,即按照对象头中的Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的阻塞线程;

3.3 偏向锁

3.3.1 概念

偏向锁(Biased Locking)是 Java HotSpot JVM 中 synchronized 关键字的一种最乐观的锁优化技术

它的核心思想是,在大多数情况下,锁不仅没有多线程竞争,而且总是由同一个线程多次获得

基于这种假设,偏向锁的目标是消除在无竞争情况下的同步开销,即使是轻量级锁所需的 CAS 操作。

DANGER

但是,在Java 15中,由于维护成本过高,偏向锁已经被标记为废弃状态,且默认不开启,详情参考:https://openjdk.org/jeps/374。

所以对于偏向锁的知识,我们了解即可。

3.3.2 程序准备

以下代码示例均在JDK 1.8下演示。

首先准备jol依赖库:

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

该库可显示对象内存的内存布局。

使用案例:

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

结果如下:

txt
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到输出了obj对象的mark word内容和klass word内容。

在JDK 1.8 下,默认开启了偏向锁,即对象创建后,对象头中的mark word最后三位为101(十六进制下为0x05),但是thread、epoch和 age都是0,但是生效是有延迟的,不会在程序启动时立即生效,如果想避免延迟,可以使用虚拟机参数-XX:BiasedLockingStartupDelay=0来禁用延迟。

添加虚拟机参数禁用偏向锁延迟后,输出结果如下:

txt
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到mark word中偏向锁标志位已经为1 了,表示已生效。

3.3.3 偏向锁示例

编写如下代码,创建一个对象obj,并且在加锁前、加锁中和解锁后打印对象内存布局:

java
public static void main(String[] args) throws InterruptedException {
    Object obj = new Object();
    System.out.println("加锁前...");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());

    synchronized (obj){
        System.out.println("加锁...");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }

    System.out.println("解锁后...");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

详细结果如下:

Details
txt
加锁前...
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁...
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000014700d805 (biased: 0x000000000051c036; epoch: 0; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

解锁后...
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000014700d805 (biased: 0x000000000051c036; epoch: 0; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

我们只需要查看Mark Word内容,整理如下:

txt
加锁前:0x0000000000000005 (biasable; age: 0)
加锁中:0x000000014700d805 (biased: 0x000000000051c036; epoch: 0; age: 0)
解锁后:0x000000014700d805 (biased: 0x000000000051c036; epoch: 0; age: 0)

可以看到obj对象的mark word在加锁后,thread 段有了内容(0x000000000051c036,即主线程ID),并且在解锁后,mark word内容也没恢复到和加锁前一样,说明obj对象偏向于主线程。

3.3.4 偏向锁的撤销

1. 有其他线程加锁

当某个锁对象已经偏向线程A后,如果线程B再对该对象加锁,则会导致该对象锁升级为轻量级锁。此处又分为两种情况:

  • 情况一:线程B加锁时,线程A已不再使用锁,则线程B加锁时会升级为轻量级锁;
  • 情况二:线程B加锁时,线程A还在使用锁,则会导致轻量级锁升级为重量级锁;

情况一示例:

Details
java
public static void main(String[] args) throws InterruptedException {
    Object obj = new Object();
    System.out.println("加锁前:" + ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj){
        System.out.println("加锁中:" + ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("解锁后:"+ClassLayout.parseInstance(obj).toPrintable());

    Thread.sleep(1000);  // 睡眠1秒,保证线程B后执行

    new Thread(()->{
        System.out.println("线程B加锁前:"+ClassLayout.parseInstance(obj).toPrintable());
        synchronized (obj){
            System.out.println("线程B加锁中:"+ClassLayout.parseInstance(obj).toPrintable());
        }
        System.out.println("线程B解锁后:"+ClassLayout.parseInstance(obj).toPrintable());
    },"B").start();

    Thread.sleep(1000);  // 睡眠1秒,保证主线程在线程B运行完后执行

    System.out.println("加锁前:" + ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj){
        System.out.println("加锁中:" + ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("解锁后:"+ClassLayout.parseInstance(obj).toPrintable());

}

完整结果如下:

Details
txt
加锁前:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁中:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000010f808805 (biased: 0x000000000043e022; epoch: 0; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

解锁后:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000010f808805 (biased: 0x000000000043e022; epoch: 0; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

线程B加锁前:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000010f808805 (biased: 0x000000000043e022; epoch: 0; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

线程B加锁中:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00000001729f2950 (thin lock: 0x00000001729f2950)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

线程B解锁后:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁前:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁中:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000016fd72a10 (thin lock: 0x000000016fd72a10)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

解锁后:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

经过分析,在线程A(代码示例中的主线程)加锁时,是轻量级锁;当线程B加锁时,此时线程A已释放了锁,所以线程B加的是轻量级锁;在线程B之后,线程A再加锁,仍然是轻量级锁。

情况二示例:

Details
java
public static void main(String[] args) throws InterruptedException {
    Object obj = new Object();

    new Thread(()->{
        System.out.println(LocalDateTime.now() + " A加锁前:"+ClassLayout.parseInstance(obj).toPrintable());
        synchronized (obj){
            try {
                System.out.println(LocalDateTime.now() + " A加锁时1:"+ClassLayout.parseInstance(obj).toPrintable());
                Thread.sleep(2000);
                System.out.println(LocalDateTime.now() + " A加锁时2:"+ClassLayout.parseInstance(obj).toPrintable());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(LocalDateTime.now() + " A解锁后:"+ClassLayout.parseInstance(obj).toPrintable());
    },"A").start();

    Thread.sleep(1000);
    new Thread(()->{
        System.out.println(LocalDateTime.now() + " B加锁前:"+ClassLayout.parseInstance(obj).toPrintable());
        synchronized (obj){
            System.out.println(LocalDateTime.now() + " B加锁时:"+ClassLayout.parseInstance(obj).toPrintable());
        }
        System.out.println(LocalDateTime.now() + " B加锁后:"+ClassLayout.parseInstance(obj).toPrintable());
    },"B").start();
}

完整结果如下:

Details
txt
2025-05-03T10:01:33.821 A加锁前:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2025-05-03T10:01:34.274 A加锁时1:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000012a064805 (biased: 0x00000000004a8192; epoch: 0; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2025-05-03T10:01:34.813 B加锁前:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000012a064805 (biased: 0x00000000004a8192; epoch: 0; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2025-05-03T10:01:36.279 A加锁时2:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000011980d492 (fat lock: 0x000000011980d492)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2025-05-03T10:01:36.281 A解锁后:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000011980d492 (fat lock: 0x000000011980d492)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2025-05-03T10:01:36.281 B加锁时:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000011980d492 (fat lock: 0x000000011980d492)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2025-05-03T10:01:36.283 B加锁后:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000011980d492 (fat lock: 0x000000011980d492)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以发现,如果在线程A持有锁的期间,线程B对同一个锁对象加锁,那么偏向锁会升级为重量级锁。

2. 调用了hashCode()

在无锁状态下,hashCode存储在对象头中,偏向锁也存储在对象头中,所以两者冲突,必然导致偏向锁升级。此处又分为两种情况讨论:

  • 情况一:在加锁前调用hashCode()方法,那么会导致偏向锁升级为轻量级锁。
  • 情况二:在加锁过程中调用hashCode()方法,那么会导致偏向锁升级为重量级锁。

情况一代码示例:

Details
java
public static void main(String[] args) {
    Object obj = new Object();
    System.out.println("加锁前 hashCode前:" + ClassLayout.parseInstance(obj).toPrintable());
    obj.hashCode();
    System.out.println("加锁前 hashCode后:" + ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj){
        System.out.println("加锁中:" + ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("解锁后:"+ClassLayout.parseInstance(obj).toPrintable());

}

结果如下:

txt
加锁前 hashCode前:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁前 hashCode后:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000004b53f53801 (hash: 0x4b53f538; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁中:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000016e04aa20 (thin lock: 0x000000016e04aa20)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

解锁后:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000004b53f53801 (hash: 0x4b53f538; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,在加锁前调用hashCode()方法,会导致直接加轻量级锁,而不是偏向锁。

情况二代码示例:

Details
java
public static void main(String[] args) {
    Object obj = new Object();
    System.out.println("加锁前:" + ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj){
        System.out.println("加锁中 hashCode前:" + ClassLayout.parseInstance(obj).toPrintable());
        obj.hashCode();
        System.out.println("加锁中 hashCode后:" + ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("解锁后:"+ClassLayout.parseInstance(obj).toPrintable());

}

结果如下:

txt
加锁前:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁中 hashCode前:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000139009805 (biased: 0x00000000004e4026; epoch: 0; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁中 hashCode后:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000012880be92 (fat lock: 0x000000012880be92)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

解锁后:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000012880be92 (fat lock: 0x000000012880be92)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

在加锁过程中调用锁对象的hashCode()方法,会导致偏向锁直接升级为重量级锁。

3. 调用wait()或notify()方法

关于wait()notify()的用法,会在下一章着重讲解。

由于wait()notify()是和Monitor对象关联的,所以调用了wait()notify()方法后,会导致偏向锁升级为重量级锁。

代码示例如下:

Details
java
public static void main(String[] args) throws InterruptedException {
    Object obj = new Object();

    new Thread(()->{
        System.out.println("加锁前:" + ClassLayout.parseInstance(obj).toPrintable());
        synchronized (obj){
            System.out.println("加锁中 wait前:" + ClassLayout.parseInstance(obj).toPrintable());
            try {
                obj.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("加锁中 wait后:" + ClassLayout.parseInstance(obj).toPrintable());
        }
        System.out.println("解锁后:" + ClassLayout.parseInstance(obj).toPrintable());
    },"A").start();

    Thread.sleep(1000);  // 确保在A线程之后执行
    synchronized (obj){
        obj.notify();
    }

}

结果如下:

txt
加锁前:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁中 wait前:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000012a8e7005 (biased: 0x00000000004aa39c; epoch: 0; age: 0)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加锁中 wait后:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000012c01bc92 (fat lock: 0x000000012c01bc92)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

解锁后:java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000012c01bc92 (fat lock: 0x000000012c01bc92)
  8   4        (object header: class)    0x00000f28
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

3.3.5 批量重偏向和消除

批量重偏向是指当同一类的很多对象都偏向于同一个线程 t1,但这些对象现在频繁(超过某个阈值)被另一个线程 t2 尝试获取锁,导致频繁发生偏向锁撤销,由于偏向锁撤销是一个相对重量级的操作,通常需要在安全点(Safepoint)进行,所以JVM 会修改该类的元数据,当超过阈值后线程t2尝试获取锁时,JVM 不会再尝试撤销对线程 t1 的旧偏向。相反,它会尝试将对象直接偏向于当前正在获取锁的线程(线程 t2)。

批量消除是指某一个类的很多对象频繁发生批量重偏向,JVM会直接将该类对象的偏向锁功能锁定,使得该类的现有对象直接进入轻量级锁状态,新对象直接进入无锁状态。

代码示例:现有线程t1对Object类的多个对象加锁,此时是偏向锁,锁对象偏向于t1线程。t1线程运行结束后,t2线程对这些Object对象加锁,此时会先发生锁撤销(即升级为轻量级锁),但是超过一定阈值后,会将剩余对象的锁状态直接偏向t2线程。t2线程运行结束后,t3线程仍然对这些Object对象加锁,此时仍然会先发生锁撤销(升级为轻量级锁),但是由于该类对象撤销次数过多(超过另一个更高的阈值),JVM会将该类对象设置为不可偏向,则最终这些对象均为轻量级锁。最后,主线程新建一个Object对象,此时发现新建的对象是无锁状态,即禁用了该类对象的偏向锁。

注意:如果一个对象是无锁状态(001),那么对该对象加锁,不会加轻量级锁,而是会从轻量级锁开始。

注意:下面代码使用了附录中的工具类MarkWordUtil

Details
java
public static void main(String[] args) throws InterruptedException {

    Thread t1,t2,t3;
    Vector<Object> vector = new Vector<>();
    final int loopNumber = 40;

    t1 = new Thread(()->{
        for (int i = 0; i < loopNumber; i++) {
            Object obj = new Object();
            vector.add(obj);
            synchronized (obj){
                MarkWordUtil.printMarkWord(obj);
            }
        }

        synchronized (vector){
            vector.notify();
        }
    },"t1");
    t1.start();

    t2 = new Thread(()->{
        synchronized (vector){
            try {
                vector.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        System.out.println("==================");
        for (int i = 0; i < loopNumber; i++) {
            synchronized (vector.get(i)){
                MarkWordUtil.printMarkWord(vector.get(i));
            }
        }

        synchronized (Vector.class){
            Vector.class.notify();
        }
    },"t2");
    t2.start();

    t3 = new Thread(()->{
        synchronized (Vector.class){
            try {
                Vector.class.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println("==================");
            for (int i = 0; i < loopNumber; i++) {
                synchronized (vector.get(i)){
                    MarkWordUtil.printMarkWord(vector.get(i));
                }
            }
        }

    },"t3");
    t3.start();

    t3.join();
    System.out.println("==================");
    MarkWordUtil.printMarkWord(new Object());
}

结果:

txt
2025-05-03T11:39:56.878 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.878 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.879 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.879 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.880 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.880 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.880 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.881 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.881 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.881 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.881 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.882 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.882 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.882 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.883 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.883 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.883 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.883 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.883 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.885 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.885 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.885 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.885 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.885 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.886 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.886 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.888 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.888 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
==================
2025-05-03T11:39:56.888 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.889 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.889 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.889 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.889 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.889 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.890 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.890 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.890 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.890 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.891 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.891 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.891 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.891 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.891 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.892 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.892 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.892 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.892 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.893 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.893 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.893 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.893 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.896 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.896 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.896 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.896 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.897 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
==================
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.902 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.902 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.902 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.902 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.902 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
==================
2025-05-03T11:39:56.902 [main] 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

3.4 小结

使用synchronized对对象加锁,从偏向锁-->轻量级锁-->重量级锁,效率逐渐降低。

4. 锁重入

所谓锁重入,是指某个线程t1对某个对象A加锁,在外层同步代码块内,还可以对该对象加锁,用代码表示如下:

java
public static void main(String[] args) {
    Object obj = new Object();
    synchronized (obj){
        synchronized (obj){
            System.out.println("hello");
        }
    }
}

"hello"正常输出,说明synchronized的锁是可以重入的。

4.1 重量级锁的锁重入

重量级锁的锁重入是通过Monitor对象的Count来实现的,Count 字段是一个计数器,用来记录当前持有 Monitor 的线程重入(reenter)这个锁的次数。

当线程A第一次持有锁时:会将锁对象的Monitor对象中的Owner字段指向当前线程(即线程A),并将 _count 字段设置为 1。

当线程A在持有锁对象的时候,再次获取同一对象的锁时,并不会导致线程A的阻塞,而是会把Count字段的值加1。

当线程A释放锁时,会将所对象的Monitor对象中的Count字段的值减1:

  • 如果 Count 递减后仍然大于 0,说明线程 A 还有其他层级的重入锁没有释放。线程 A 仍然持有锁,Owner 字段保持不变。

  • 如果 Count 递减后等于 0,说明线程 A 已经完全退出了这个锁的最外层同步块。线程 A 不再持有锁。JVM 会将 Owner 字段设置为空,然后检查是否有其他线程在等待队列 (EntryList) 中,如果有,就会唤醒等待的线程来竞争锁。

简单来说,重量级锁的重入机制就是依靠 Monitor 内部的一个计数器。持有锁的线程每次“重新进入”同步块时,计数器加一;每次退出同步块时,计数器减一。只有当计数器归零时,锁才会被真正释放,其他等待的线程才有机会获取锁。

4.2 轻量级锁的锁重入

轻量级锁的锁重入是通过创建多个锁记录(Lock Record)来实现的,当某个线程多次获取同一个对象的轻量级锁时,会创建多个锁记录。

当线程A第一次持有锁时,会在线程A的栈中创建一个锁记录,并且对象头中的Mark Word指向该锁记录地址:

image-20250503125133494

当线程A在持有对象的轻量级锁时,再次获取该对象的锁,JVM会在栈中创建一个新的锁记录,新的锁记录的 displaced_header 字段不会复制对象的 Mark Word,而是设置为一个特殊的标记值(通常是 null ),用来表示这是一个重入锁记录。并且不会再次尝试修改对象的 Mark Word,对象的 Mark Word 仍然指向第一次获取锁时创建的那个锁记录

image-20250503125415689

当释放锁时,通过检查锁记录的displaced header的值,就能判断该锁是不是重入的锁:

  • 当线程退出一个 synchronized 代码块时,JVM 会找到当前栈帧中与该对象关联的最顶层的锁记录
  • 如果锁记录的 displaced_header 字段是那个特殊的重入标记 (null): 说明这是一个重入锁记录。JVM 直接将这个锁记录从栈中弹出/丢弃。对象的 Mark Word 不变。线程仍然持有锁。
  • 如果锁记录的 displaced_header 字段是原始的对象 Mark Word 值: 说明这是该线程为这个对象创建的**最底层的(第一次)**锁记录,按照轻量级锁的解锁步骤进行解锁。

轻量级锁的重入依赖于线程栈上的锁记录链。每次重入都在栈上增加一个标记为重入的锁记录。对象的 Mark Word 只指向最底层的锁记录。释放锁时,从栈顶开始逐个弹出锁记录,直到弹出带有原始 Mark Word 的锁记录时,才尝试通过 CAS 恢复对象头,完成锁的真正释放。

4.3 偏向锁的锁重入

偏向锁记录了线程ID,天生是可以重入的。

5. 自旋优化

当发生锁竞争的时候,可以使用自旋来进行优化。所谓自旋,就是进行几次无意义的循环,以期待在循环期间,其他线程释放了锁,这时当前线程就可以避免阻塞。

注意

只有在多CPU的机器上,自旋优化才有意义。

以下是自旋成功的图示:

image-20250503125952650

6. 锁消除

锁消除(Lock Elimination)是 Java 虚拟机(JVM)的即时编译器(JIT Compiler)在运行时进行的一种优化技术。

它的核心思想是:如果 JVM 经过分析,发现一个对象虽然使用了同步机制(比如 synchronized),但实际上它不可能被多个线程共享访问,那么这个对象的锁操作就是完全不必要的,可以直接将这些锁操作消除掉

例如:

java
public void method(){
    Object obj = new Object();
    synchronized (obj){
        // 同步代码块
    }
}

在上面的方法中创建了局部变量obj,但是该局部变量没有发生逃逸,因此是线程私有的,所以对其加锁是没有意义的,所以JVM会在运行时,将锁操作消除。

附录

MarkWordUtil 工具类

MarkWordUtil 工具类只打印对象头,而不输出其他内容:

java
import org.openjdk.jol.info.ClassLayout;

import java.time.LocalDateTime;

public class MarkWordUtil {
    public static void printMarkWord(Object o) {
        ClassLayout classLayout = ClassLayout.parseInstance(o);
        String printable = classLayout.toPrintable();
        String[] split = printable.split("\n");
        String substring = split[2].substring(split[2].indexOf("0x"));
        System.out.println(LocalDateTime.now() + " [" +Thread.currentThread().getName() + "] " + hexToBinaryWithSpaces(substring.substring(0, 18)));
    }

    private static String hexToBinaryWithSpaces(String hexString) {
        // 去掉可能存在的"0x"前缀
        String cleanHexString = hexString.startsWith("0x") ? hexString.substring(2) : hexString;

        StringBuilder binaryBuilder = new StringBuilder();

        // 将每个十六进制字符转换为4位二进制
        for (char hexChar : cleanHexString.toCharArray()) {
            String binaryChar = Integer.toBinaryString(Character.digit(hexChar, 16));
            // 确保每4位二进制数是4位长度(前面补0)
            binaryBuilder.append(String.format("%4s", binaryChar).replace(' ', '0'));
        }

        // 插入空格使得每8个二进制字符间有一个空格
        StringBuilder resultBuilder = new StringBuilder();
        for (int i = 0; i < binaryBuilder.length(); i += 8) {
            if (i > 0 && i % 8 == 0) {
                resultBuilder.append(" ");
            }
            resultBuilder.append(binaryBuilder.substring(i, Math.min(i + 8, binaryBuilder.length())));
        }

        return resultBuilder.toString().trim(); // 移除可能的尾随空格
    }
}