前言

本文承接【Java 并发之 synchronized 关键字深度解析(一)】,着重介绍 synchronized 几种锁的特性及其底层实现原理。

一、对象头结构及锁状态标识

synchronized 关键字是如何实现给对象加锁的?首先我们需要了解 Java 中对象的组成。Java 对象在内存中主要由三部分组成:对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。

  • 对齐填充:JVM 规定对象的起始内存地址必须是 8 字节的整数倍,如果不够则用占位符填充,此部分即为对齐填充。
  • 实例数据:对象存储的真正有效信息,即对象的成员变量信息(包括继承自父类的)。
  • 对象头:由两部分组成。第一部分是对象的运行时数据(Mark Word),包括哈希码、锁偏向标识、锁类型、GC 分代年龄、偏向线程 ID 等;第二部分是对象的类型指针(Klass Word),用于在堆中定位对象的实例数据和方法区中的类型数据。Java 对象的公共特性都存放在对象头中。

对象头存储内容如下所示(以 64 位操作系统为例):

|--------------------------------------------------------------------------------------------------------------|
|                                              Object Header (128 bits)                                        |
|--------------------------------------------------------------------------------------------------------------|
|                        Mark Word (64 bits)                                    |      Klass Word (64 bits)    |       
|--------------------------------------------------------------------------------------------------------------|
|  unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |     OOP to metadata object   |  无锁
|----------------------------------------------------------------------|--------|------------------------------|
|  thread:54 |         epoch:2      | unused:1 | age:4 | biased_lock:1 | lock:2 |     OOP to metadata object   |  偏向锁
|----------------------------------------------------------------------|--------|------------------------------|
|                     ptr_to_lock_record:62                            | lock:2 |     OOP to metadata object   |  轻量锁
|----------------------------------------------------------------------|--------|------------------------------|
|                     ptr_to_heavyweight_monitor:62                    | lock:2 |     OOP to metadata object   |  重量锁
|----------------------------------------------------------------------|--------|------------------------------|
|                                                                      | lock:2 |     OOP to metadata object   |    GC
|--------------------------------------------------------------------------------------------------------------|

其中 lock:2 表示有 2 bit 控制锁类型,biased_lock:1 表示 1 bit 控制偏向锁状态,对应关系如下所示:

  • 01:无锁(前面偏向锁状态为 0 时表示未锁定)
  • 01:可偏向(前面偏向锁状态为 1 时表示可偏向)
  • 00:轻量级锁
  • 10:重量级锁
  • 11:GC 标记

看到前两种状态时读者可能会有些疑惑。JVM 的设计者想用 01 状态来表示两种情况(无锁和可偏向),但显然一个字符无法标识两种状态。因此,他们将前面一位暂时用不到的 bit 纳入进来,用前一位的值是 0 还是 1 来区分是无锁还是可偏向。

二、锁的信息打印

下面我们通过代码验证这几种锁的存在。JVM 默认开启偏向锁,默认的偏向锁启动时间为 4-5 秒后。因此,先让主线程睡眠 5 秒再加锁能保证对象处于偏向锁的状态。此外,也可以在 VM Options 中添加参数 -XX:BiasedLockingStartupDelay=0 来让 JVM 取消延迟启动偏向锁(本文的示例均未设置此参数),其效果跟不改变 VM Options 只在 main 方法中让主线程先睡眠 5 秒是一样的。

此外,要打印对象存储空间需要引入 OpenJDK 的 jol-core 包依赖:

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

User 对象代码:

public class LockClient {
    static class User {
        public String name;
        public byte age;
    }
}

万事俱备,下面开始测试。

1. 无锁状态

先不睡眠五秒,此时偏向锁未开启,所以对象都是无锁状态(未加 synchronized 的情况下)。打印无锁状态的对象(锁标识 001):

@Test
public void noLock() {
    User user = new User();
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
}

输出结果:

com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

下面我们来解读一下这个打印结果。

通过 TYPE DESCRIPTION 可以知道,前三行打印的是对象头(object header),那么后面四行就是对象的实例数据和对其填充了。

先看第一行,VALUE 中,标红的 001 表示当前对象是无锁状态,前面的 0 对应我们上面讲的可偏向锁状态为非偏向锁(如果是 1 表示偏向锁)。第三行存放的是对象指针。

第四行和第六行存放的是对象的两个成员变量,第五行空间用于填充 age 变量;第七行就是我们所说的对齐填充,使对象内存空间凑齐 8 字节的整数倍。

2. 偏向锁状态

加上睡眠 5 秒:

@Test
public void biasedLocking() {
    // 先睡眠 5 秒,保证开启偏向锁
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) { // -XX:-UseBiasedLocking
        e.printStackTrace(); // -XX:BiasedLockingStartupDelay=0
    }
    User user = new User();
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
}

看看打印结果:

com.daicy.jvm.LockClient$User 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)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

可以看到,锁状态为 101 可偏向锁状态了,只是由于未用 synchronized 加锁,所以线程 ID 是空的。其余数据跟上述无锁状态一样。

偏向锁带线程 ID 情况,代码如下:

@Test
public void synchronizedLocking() {
    // 先睡眠 5 秒,保证开启偏向锁
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) { // -XX:-UseBiasedLocking
        e.printStackTrace(); // -XX:BiasedLockingStartupDelay=0
    }
    System.out.println(Thread.currentThread().getId());
//        System.out.println(Integer.toBinaryString(System.identityHashCode(Thread.currentThread())));
    User user = new User();
    synchronized (user) {
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
}

输出结果:

com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 d8 00 fc (00000101 11011000 00000000 11111100) (-67053563)
      4     4                    (object header)                           0e 7f 00 00 (00001110 01111111 00000000 00000000) (32526)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

可见第一行中后面不再是 0 了,有了线程 ID 的值。

3. 轻量级锁状态

再看看轻量锁,不睡眠 5 秒,直接用 synchronized 给对象加锁,此时触发的就是轻量锁。代码如下:

@Test
public void lightWeightLock() {
    System.out.println(Integer.toBinaryString(System.identityHashCode(Thread.currentThread())));
    User user = new User();
    synchronized (user) {
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
}

打印结果:

com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           e0 59 8b 34 (11100000 01011001 10001011 00110100) (881547744)
      4     4                    (object header)                           77 7f 00 00 (01110111 01111111 00000000 00000000) (32631)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

可以看到锁的标识位为 000,轻量级锁。

4. 重量级锁状态

最后看一下重量级锁,只有在锁竞争的时候才会变为重量级锁,代码如下:

@Test
public void heavyWeightLock() {
    User user = new User();
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
    Thread t1 = new Thread(() -> {
        synchronized (user) {
            try {
                Thread.sleep(5000);// 睡眠,创造竞争条件
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();
    Thread t2  = new Thread(() -> {
        synchronized (user) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t2.start();
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
}

输出结果为:

com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           6a 62 00 50 (01101010 01100010 00000000 01010000) (1342202474)
      4     4                    (object header)                           74 7f 00 00 (01110100 01111111 00000000 00000000) (32628)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

可以看到锁状态为 010,重量级锁。

5. 调用 hashCode 会取消偏向

此外,如果通过 Object 对象的本地 hashCode 方法来获取对象的 hashCode 值,会使对象取消偏向锁状态。

@Test
public void cancelBiasedLocking() {
    // 先睡眠 5 秒,保证开启偏向锁
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) { // -XX:-UseBiasedLocking
        e.printStackTrace(); // -XX:BiasedLockingStartupDelay=0
    }
    User user = new User();
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
    System.out.println(user.hashCode());
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
    synchronized (user) {
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
}

打印结果:

com.daicy.jvm.LockClient$User 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)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

1644443712
com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 40 38 04 (00000001 01000000 00111000 00000100) (70795265)
      4     4                    (object header)                           62 00 00 00 (01100010 00000000 00000000 00000000) (98)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           e0 29 99 e8 (11100000 00101001 10011001 11101000) (-392615456)
      4     4                    (object header)                           a8 7f 00 00 (10101000 01111111 00000000 00000000) (32680)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

可以看到,计算完对象的 hashCode 之后,该对象立即从偏向锁状态变为了无锁状态。即使后续给对象加锁,该对象也只会进入轻量级或者重量级锁状态,不会再进入偏向状态了。因为该对象一旦进行 Object 的 hashCode 计算,那么对象头中会保存这个 hashCode,此时再也无法存放偏向线程的 ID 了(因为对象头的长度无法同时存放 hashCode 和偏向线程 ID),所以此后该对象无法再进入偏向锁状态。

三、锁膨胀过程

到这里,我们一起看完了 synchronized 给对象加的各种锁状态以及触发场景,下面我们梳理一下它们之间的关系。

JVM 启动后会默认开启偏向锁(默认 4-5 秒后开启),开启后,所有新建对象的对象头中都标识为 101 可偏向状态,且偏向线程 ID 为 0,表示处于初始化的偏向锁状态。此后一旦有线程对该对象使用了 synchronized 加锁,那么就会进入偏向锁状态,偏向线程 ID 记录当前线程 ID;如果走完同步块之后,有另一个线程对该对象加锁,那么膨胀为轻量级锁;如果未走完同步块就有另一个线程试图给该对象加锁,那么会直接膨胀为重量级锁(中间会有一个自旋锁的过程,此处略去)。

1. 开启偏向锁

开启偏向的锁膨胀草图

下面演示一下对象从偏向锁膨胀为轻量级锁的过程:

@Test
public void biasedLockToLightWeightLock() throws InterruptedException {
    // 先睡眠 5 秒,保证开启偏向锁
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) { // -XX:-UseBiasedLocking
        e.printStackTrace(); // -XX:BiasedLockingStartupDelay=0
    }
    User user = new User();
    Thread t1 = new Thread(() -> {
        synchronized (user) {
            System.out.println(ClassLayout.parseInstance(user).toPrintable());
        }
    });
    t1.start();
    t1.join(); // 确保 t1 执行完了再执行当前主线程
    synchronized (user) {
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
}

打印结果如下,可以看到 user 对象先是偏向锁,然后变为轻量级锁,最后走完同步块释放锁变为无锁状态。

com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 38 3a 1c (00000101 00111000 00111010 00011100) (473577477)
      4     4                    (object header)                           7d 7f 00 00 (01111101 01111111 00000000 00000000) (32637)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           d8 a9 de 25 (11011000 10101001 11011110 00100101) (635349464)
      4     4                    (object header)                           7d 7f 00 00 (01111101 01111111 00000000 00000000) (32637)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

2. 关闭偏向锁

如果通过参数设置 JVM 不开启偏向锁,那么新创建的对象是 001 无锁状态,遇到 synchronized 同步块会变为轻量级锁,遇到锁竞争变为重量级锁。

@Test
public void lightWeightToheavyWeightLock() {
    //-XX:-UseBiasedLocking 关闭偏向锁
    User user = new User();
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
    Thread t1 = new Thread(() -> {
        synchronized (user) {
            try {
                Thread.sleep(5000);// 睡眠,创造竞争条件
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
    Thread t2  = new Thread(() -> {
        synchronized (user) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t2.start();
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           20 e9 ff 17 (00100000 11101001 11111111 00010111) (402647328)
      4     4                    (object header)                           8e 7f 00 00 (10001110 01111111 00000000 00000000) (32654)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

com.daicy.jvm.LockClient$User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           1a 63 00 40 (00011010 01100011 00000000 01000000) (1073767194)
      4     4                    (object header)                           8e 7f 00 00 (10001110 01111111 00000000 00000000) (32654)
      8     4                    (object header)                           24 f3 00 f8 (00100100 11110011 00000000 11111000) (-134155484)
     12     1               byte User.age                                  0
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.String User.name                                 null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

关闭偏向的锁膨胀草图

四、重量级锁原理

Java 中 synchronized 的重量级锁,是基于进入和退出 Monitor 对象实现的。在编译时会将同步块的开始位置插入 monitorenter 指令,在结束位置插入 monitorexit 指令。当线程执行到 monitorenter 指令时,会尝试获取对象所对应的 Monitor 所有权,如果获取到了,即获取到了锁,会在 Monitor 的 owner 中存放当前线程的 ID,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个 Monitor。

测试代码:
https://github.com/daichangya/Dtutorials/tree/main/jvm/src/main/java/com/daicy/jvm


说明:本文内容基于 HotSpot JVM 早期版本(Java 14 及之前),其中偏向锁(Biased Locking)在 Java 15 中被 deprecated,并在 Java 16 中正式移除。在新版本 JVM 中,锁升级过程可能不再包含偏向锁阶段。