Bootstrap

深入浅出偏向锁

在 JDK 1.5 之前,Java 并发编程中的 synchronized 关键字是解决多线程同步问题的主要手段。通过 synchronized 关键字,Java 提供了三种同步方式:

  1. 同步方法:锁上当前实例对象。
  2. 同步静态方法:锁上当前类的 Class 对象。
  3. 同步块:锁上代码块里面配置的对象。

例如,下面的代码展示了如何使用同步块:

public void test() {
    synchronized (object) {
        i++;
    }
}

通过 javap -v 编译后的指令可以看到,monitorenter 指令会插入到同步代码块的开始位置,而 monitorexit 指令会插入到方法结束和异常的位置(实际隐藏了 try-finally)。每个对象都有一个 monitor 与之关联,当一个线程执行到 monitorenter 指令时,就会获得对象所对应 monitor 的所有权,也就获得到了对象的锁。

在这里插入图片描述

1 对象监视器 monitor

在 Java 中,monitor 可以被看作是一种守门人或保安,它确保同一时刻只有一个线程可以访问受保护的代码段。monitor 的工作方式如下:

  • 进入房间:当一个线程想要进入受保护的代码区域(房间)时,它必须得到 monitor 的允许。如果房间里没有其他线程,monitor 会让它进入并关闭门。
  • 等待其他线程:如果房间里已经有一个线程,其他线程就必须等待。monitor 会让其他线程排队等候,直到房间里的线程完成工作离开房间。
  • 离开房间:当线程完成它的工作并离开受保护的代码区域时,monitor 会重新打开门,并让等待队列中的下一个线程进入。
  • 协调线程monitor 还可以通过一些特殊的机制(例如 waitnotify 方法)来协调线程之间的合作。

2 重量级锁

当另一个线程执行到同步块时,由于它没有对应 monitor 的所有权,就会被阻塞,此时控制权只能交给操作系统,从用户模式切换到内核模式,由操作系统来负责线程间的调度和线程的状态变更。这种频繁的上下文切换会引起很大的开销,因此被称为重量级锁。

3 轻量级锁

如果 CPU 通过 CAS(Compare-And-Swap)就能处理好加锁/释放锁,这样就不会有上下文的切换。但是当竞争很激烈时,CAS 尝试再多也是浪费 CPU,权衡一下,不如升级成重量级锁,阻塞线程排队竞争,也就有了轻量级锁升级成重量级锁的过程。

4 偏向锁

HotSpot 的作者研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。同一个线程反复获取锁,如果还按照 CAS 的方式获取锁,也是有一定代价的。如何让这个代价更小一些呢?

偏向锁实际上就是「锁对象」潜意识「偏向」同一个线程来访问,让锁对象记住这个线程 ID,当线程再次获取锁时,亮出身份,如果是同一个 ID 直接获取锁就好了,是一种 load-and-test 的过程,相较 CAS 又轻量级了一些。

可是多线程环境,也不可能只有同一个线程一直获取这个锁,其他线程也是要干活的,如果出现多个线程竞争的情况,就会有偏向锁升级的过程。

4.1 偏向锁的升级过程

偏向锁可以绕过轻量级锁,直接升级到重量级锁吗?答案是可以的。偏向锁的升级过程如下:

  1. 无竞争的情况下,只有一个线程进入临界区,采用偏向锁。
  2. 多个线程可以交替进入临界区,采用轻量级锁。
  3. 多线程同时进入临界区,交给操作系统互斥量来处理,升级为重量级锁。

4.2 Java 对象头

在 Java 中,对象头(Object Header)是对象在内存中的布局的一部分,它包含了对象的元数据信息。对象头中的 MarkWord 是保存锁状态的关键部分,它记录了对象的锁状态、哈希码、GC 信息等。通过对象头,Java 可以高效地管理对象的锁状态,从而实现线程同步。

4.2.1 对象头的结构

Java 对象头最多由三部分构成:

  1. MarkWord:保存对象的锁状态、哈希码、GC 信息等。
  2. ClassMetadata Address:指向对象的类元数据的指针。
  3. Array Length(如果对象是数组才会有这部分):数组的长度。

其中,MarkWord 是保存锁状态的关键部分。对象的锁状态可以从无锁状态升级到偏向锁、轻量级锁,最后到重量级锁。

4.2.2 偏向锁的延迟开启

在 JDK 1.6 之后,默认是开启偏向锁的,但开启有延迟,大约 4 秒。这是因为在 JVM 内部有很多地方使用了 synchronized,如果直接开启偏向锁,产生竞争时就需要锁升级,这会带来额外的性能损耗。因此,JVM 采用了延迟策略。

你可以通过参数 -XX:BiasedLockingStartupDelay=0 将延迟改为 0,但不建议这么做。

4.2.3 使用 JOL 查看对象头

为了更好地理解对象头的变化,我们可以使用 JOL(Java Object Layout)工具来查看对象的内存布局。

4.2.3.1 引入 JOL 依赖

首先,在 pom.xml 中引入 JOL 依赖:

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.16</version>
</dependency>
4.2.3.2 场景 1:默认延迟下的对象头变化
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

@Slf4j
public class SynchronizedBlockTest {
    public static void main(String[] args) {
        Object o = new Object();
        log.info("未进入同步块,MarkWord 为:");
        log.info(ClassLayout.parseInstance(o).toPrintable());
        synchronized (o) {
            log.info("进入同步块,MarkWord 为:");
            log.info(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

输出结果:

15:55:47.551 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - 1. 未进入同步块,MarkWord 为:
15:55:49.828 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - 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)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:55:49.829 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - 2. 进入同步块,MarkWord 为:
15:55:49.829 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000000318f7c8 (thin lock: 0x000000000318f7c8)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
4.2.3.3 场景 2:延迟 5 秒后的对象头变化
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

@Slf4j
public class SynchronizedBlockTest {
    public static void main(String[] args) throws InterruptedException {
        // 睡眠 5s
        Thread.sleep(5000);
        Object o = new Object();
        log.info("未进入同步块,MarkWord 为:");
        log.info(ClassLayout.parseInstance(o).toPrintable());
        synchronized (o) {
            log.info("进入同步块,MarkWord 为:");
            log.info(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

输出结果:

15:56:41.697 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - 1. 未进入同步块,MarkWord 为:
15:56:43.876 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:56:43.876 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - 2. 进入同步块,MarkWord 为:
15:56:43.877 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000002b2f0b8 (thin lock: 0x0000000002b2f0b8)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
4.2.3.4 场景 3:多线程竞争下的对象头变化
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

@Slf4j
public class SynchronizedBlockTest {
    public static void main(String[] args) throws InterruptedException {
        // 睡眠 5s
        Thread.sleep(5000);
        Object o = new Object();
        log.info("未进入同步块,MarkWord 为:");
        log.info(ClassLayout.parseInstance(o).toPrintable());
        synchronized (o) {
            log.info("进入同步块,MarkWord 为:");
            log.info(ClassLayout.parseInstance(o).toPrintable());
        }

        Thread t2 = new Thread(() -> {
            synchronized (o) {
                log.info("新线程获取锁,MarkWord为:");
                log.info(ClassLayout.parseInstance(o).toPrintable());
            }
        });

        t2.start();
        t2.join();
        log.info("主线程再次查看锁对象,MarkWord为:");
        log.info(ClassLayout.parseInstance(o).toPrintable());

        synchronized (o) {
            log.info("主线程再次进入同步块,MarkWord 为:");
            log.info(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

输出结果:

15:57:29.803 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - 1. 未进入同步块,MarkWord 为:
15:57:31.926 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:57:31.926 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - 2. 进入同步块,MarkWord 为:
15:57:31.927 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000002e8f7a0 (thin lock: 0x0000000002e8f7a0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:57:31.928 [Thread-0] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - 3. 新线程获取锁,MarkWord为:
15:57:31.928 [Thread-0] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00000000231bf4c0 (thin lock: 0x00000000231bf4c0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:57:31.928 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - 4. 主线程再次查看锁对象,MarkWord为:
15:57:31.929 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - 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)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:57:31.929 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - 5. 主线程再次进入同步块,MarkWord 为:
15:57:31.929 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.synchronizedBlockTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000002e8f7a0 (thin lock: 0x0000000002e8f7a0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

4.3 偏向锁的撤销与批量重偏向

在 Java 中,偏向锁是一种优化机制,旨在减少无竞争情况下的锁开销。然而,当多个线程竞争同一个锁时,偏向锁可能会被撤销,从而导致性能下降。为了解决这个问题,JVM 引入了偏向锁的撤销和批量重偏向机制。

4.3.1 偏向锁的撤销

在讨论偏向锁的撤销之前,我们需要明确两个概念:

  1. 偏向锁撤销:当多个线程竞争同一个锁时,偏向锁不能再使用偏向模式,需要撤销偏向锁。
  2. 偏向锁释放:当 synchronized 方法或块结束时,锁会被释放。

何为偏向撤销?

偏向撤销是指将偏向锁的状态从偏向状态撤回到原来的状态,即将 MarkWord 的第 3 位(是否偏向撤销)的值从 1 变回 0。偏向锁的撤销只能发生在有竞争的情况下,因为如果只有一个线程获取锁,再加上「偏心」的机制,是没有理由撤销偏向的。

撤销过程

想要撤销偏向锁,还不能对持有偏向锁的线程有影响,就需要等待持有偏向锁的线程到达一个 safepoint 安全点。在这个安全点,JVM 会挂起获得偏向锁的线程。在 safepoint,线程可能处于不同的状态:

  • 线程不存活,或者活着的线程退出了同步块,直接撤销偏向。
  • 活着的线程但仍在同步块之内,那就升级成轻量级锁。

4.3.2 批量重偏向(bulk rebias)

为了减少大量撤销偏向锁带来的性能损耗,JVM 引入了批量重偏向机制。批量重偏向以 class 为单位,为每个 class 维护一个偏向锁撤销计数器。当一个 class 的对象发生偏向撤销的次数达到重偏向阈值(默认 20)时,JVM 会进行批量重偏向。

BiasedLockingBulkRebiasThreshold = 20

实现方式

批量重偏向的实现方式是通过 epoch 字段。每个 class 对象会有一个对应的 epoch 字段,每个处于偏向锁状态对象的 MarkWord 中也有该字段,其初始值为创建该对象时 class 中的 epoch 的值。

每次发生批量重偏向时,就将该值加 1,同时遍历 JVM 中所有线程的栈:

  • 找到该 class 所有正处于加锁状态的偏向锁对象,将其 epoch 字段改为新值。
  • class 中不处于加锁状态的偏向锁对象,保持 epoch 字段值不变。

这样下次获得锁时,发现当前对象的 epoch 值和 classepoch 值不同,就会直接通过 CAS 操作将其 MarkWord 的线程 ID 改成当前线程 ID,而不会执行撤销操作。

4.3.3 批量撤销(bulk revoke)

当达到重偏向阈值后,假设该 class 计数器继续增长,当其达到批量撤销的阈值(默认 40)时,JVM 就认为该 class 的使用场景存在多线程竞争,会标记该 class 为不可偏向。之后对于该 class 的锁,直接走轻量级锁的逻辑。

BiasedLockingBulkRevokeThreshold = 40

批量撤销的过渡过程

在彻底禁用偏向锁之前,JVM 还会给一次改过自新的机会。如果在距离上次批量重偏向发生的 25 秒之内,并且累计撤销计数达到 40,就会发生批量撤销(偏向锁彻底 game over)。如果在距离上次批量重偏向发生超过 25 秒之外,就会重置在 [20, 40) 内的计数,再给次机会。

4.4 偏向锁与 HashCode

在 Java 中,对象的 hashCode 是一个重要的属性,它用于对象的唯一标识。然而,hashCode 的生成和存储与对象的锁状态(如偏向锁、轻量级锁、重量级锁)之间存在一定的关系。

4.4.1 场景一:未生成 hashCode 时的对象头

public static void main(String[] args) throws InterruptedException {
    // 睡眠 5s
    Thread.sleep(5000);

    Object o = new Object();
    log.info("未生成 hashcode,MarkWord 为:");
    log.info(ClassLayout.parseInstance(o).toPrintable());

    o.hashCode();
    log.info("已生成 hashcode,MarkWord 为:");
    log.info(ClassLayout.parseInstance(o).toPrintable());

    synchronized (o) {
        log.info("进入同步块,MarkWord 为:");
        log.info(ClassLayout.parseInstance(o).toPrintable());
    }
}

运行结果

15:51:48.882 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - 未生成 hashcode,MarkWord 为:
15:51:51.104 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:51:51.104 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - 已生成 hashcode,MarkWord 为:
15:51:51.105 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000003d36e4cd01 (hash: 0x3d36e4cd; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:51:51.105 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - 进入同步块,MarkWord 为:
15:51:51.106 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00000000035df798 (thin lock: 0x00000000035df798)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

结论:即便初始化为可偏向状态的对象,一旦调用 Object::hashCode() 或者 System::identityHashCode(Object),进入同步块就会直接使用轻量级锁。

4.4.2 场景二:已偏向某一个线程,然后生成 hashCode

public static void main(String[] args) throws InterruptedException {
    // 睡眠 5s
    Thread.sleep(5000);

    Object o = new Object();
    log.info("未生成 hashcode,MarkWord 为:");
    log.info(ClassLayout.parseInstance(o).toPrintable());

    synchronized (o) {
        log.info("进入同步块,MarkWord 为:");
        log.info(ClassLayout.parseInstance(o).toPrintable());
    }

    o.hashCode();
    log.info("生成 hashcode");
    synchronized (o) {
        log.info("同一线程再次进入同步块,MarkWord 为:");
        log.info(ClassLayout.parseInstance(o).toPrintable());
    }
}

运行结果

15:50:51.550 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - 未生成 hashcode,MarkWord 为:
15:50:53.758 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:50:53.758 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - 进入同步块,MarkWord 为:
15:50:53.759 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000000345f4d0 (thin lock: 0x000000000345f4d0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:50:53.759 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - 生成 hashcode
15:50:53.759 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - 同一线程再次进入同步块,MarkWord 为:
15:50:53.759 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000000345f4d0 (thin lock: 0x000000000345f4d0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

结论:同场景一,会直接使用轻量级锁。

4.4.3 场景三:已偏向状态,在同步块中生成 hashCode

public static void main(String[] args) throws InterruptedException {
    // 睡眠 5s
    Thread.sleep(5000);

    Object o = new Object();
    log.info("未生成 hashcode,MarkWord 为:");
    log.info(ClassLayout.parseInstance(o).toPrintable());

    synchronized (o) {
        log.info("进入同步块,MarkWord 为:");
        log.info(ClassLayout.parseInstance(o).toPrintable());
        o.hashCode();
        log.info("已偏向状态下,生成 hashcode,MarkWord 为:");
        log.info(ClassLayout.parseInstance(o).toPrintable());
    }
}

运行结果

15:50:00.220 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - 未生成 hashcode,MarkWord 为:
15:50:02.410 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:50:02.410 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - 进入同步块,MarkWord 为:
15:50:02.410 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000002e7f288 (thin lock: 0x0000000002e7f288)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:50:02.410 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - 已偏向状态下,生成 hashcode,MarkWord 为:
15:50:02.411 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.HashCodeTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001cff50da (fat lock: 0x000000001cff50da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

结论:如果对象处在已偏向状态,生成 hashCode 后,就会直接升级成重量级锁。

4.4.4 小结

对象的 hashCode 生成和存储与对象的锁状态之间存在一定的关系。具体来说:

  1. 未生成 hashCode:对象的 MarkWord 状态可以是偏向锁、轻量级锁或无锁状态。
  2. 已生成 hashCode:对象的 MarkWord 状态会直接进入轻量级锁状态,即使对象之前是偏向锁状态。
  3. 已偏向状态,生成 hashCode:对象的 MarkWord 状态会直接升级成重量级锁。

4.5 重量级锁与 Object.wait

Objectwait() 方法是互斥量(重量级锁)独有的,一旦调用该方法,就会升级成重量级锁。

代码示例

以下是一个示例代码,展示了在同步块中调用 wait() 方法对锁的影响:

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

@Slf4j
public class SynchronizedBlockTest {
    public static void main(String[] args) throws InterruptedException {
        // 睡眠 5s
        Thread.sleep(5000);

        Object o = new Object();
        log.info("未生成 hashcode,MarkWord 为:");
        log.info(ClassLayout.parseInstance(o).toPrintab![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/7933dc896d804c369d3c91c12450041c.png#pic_center)
le());

        synchronized (o) {
            log.info("进入同步块,MarkWord 为:");
            log.info(ClassLayout.parseInstance(o).toPrintable());

            log.info("wait 2s");
            o.wait(2000);

            log.info("调用 wait 后,MarkWord 为:");
            log.info(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

运行结果

15:45:09.168 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.WaitTest - 未生成 hashcode,MarkWord 为:
15:45:11.344 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.WaitTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:45:11.344 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.WaitTest - 进入同步块,MarkWord 为:
15:45:11.345 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.WaitTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000000259f218 (thin lock: 0x000000000259f218)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

15:45:11.345 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.WaitTest - wait 2s
15:45:13.355 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.WaitTest - 调用 wait 后,MarkWord 为:
15:45:13.356 [main] INFO com.yunyang.javabetter.juc.pianxiangsuo.WaitTest - java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001cb66dea (fat lock: 0x000000001cb66dea)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

4.6 再见偏向锁

由于维护成本过高,JDK 15 开始,偏向锁默认被禁用。虽然偏向锁可能在未来的某个版本中被移除,但了解其工作原理仍然有助于我们理解 Java 并发编程的底层机制。

5 总结

偏向锁是 Java 并发编程中的一种优化手段,旨在减少无竞争情况下的锁开销。然而,随着 JDK 版本的演进,偏向锁的维护成本逐渐显现,最终在 JDK 15 中被默认禁用。尽管如此,理解偏向锁的工作原理仍然有助于我们更好地掌握 Java 并发编程的核心概念。

6 思维导图

在这里插入图片描述

7 参考链接

深入浅出偏向锁

;