文章目录

Java 偏向锁到轻量级锁的升级条件与撤销开销

发布于 2026-05-17 15:22:40 · 浏览 23 次 · 评论 0 条

Java 偏向锁到轻量级锁的升级条件与撤销开销

Java 对象头中的 Mark Word 是锁状态实现的核心。偏向锁设计初衷是为了优化同一线程反复获取锁的场景,但在多线程竞争出现时,必须升级为轻量级锁。这一过程并非毫无代价,理解其触发条件与撤销开销,是进行高性能 Java 并发编程的必修课。


1. 理解对象头内存布局

在分析升级过程前,必须先看懂 Mark Word 在不同锁状态下的位结构。以 64 位 JVM 为例,对象头长度为 64 bit(8 字节)。

查看 下表了解不同状态下的内存分布:

锁状态 存储内容 状态标识值
无锁 对象的 HashCode、分代年龄 01
偏向锁 线程 ID、Epoch、分代年龄 01
轻量级锁 指向栈中 Lock Record 的指针 00
重量级锁 指向堆中 Monitor 对象的指针 10

注意“无锁”和“偏向锁”的最后两位都是 01,区分它们依靠的是倒数第三位 biased_lock 标志位。


2. 准备验证环境

自 JDK 15 起,偏向锁默认被禁用。为了观察这一机制,需手动开启并确保使用合适的 JDK 版本。

添加 JVM 启动参数以开启偏向锁:

-XX:+UseBiasedLocking

引入 JOL (Java Object Layout) 工具依赖来打印对象头信息:

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

3. 触发升级的核心条件

偏向锁撤销并升级为轻量级锁,通常由以下三种情况触发。

3.1 调用 hashCode 方法

当一个对象处于偏向锁状态时,Mark Word 中存储的是线程 ID 和 Epoch,没有空间存储 31 位的 hashCode

调用 Object.hashCode() 会强制撤销偏向锁:

  1. JVM 检查 对象是否处于偏向状态。
  2. 若处于偏向状态,JVM 撤销 偏向锁。
  3. JVM 恢复 对象为无锁状态(或轻量级锁,取决于后续操作),并将 HashCode 写入 Mark Word

3.2 其它线程竞争锁

这是最常见的升级场景。线程 A 持有偏向锁,线程 B 尝试获取同一个锁。

分析 竞争时的执行流程:

  1. 线程 B 访问 对象头。
  2. 线程 B 发现 对象处于偏向状态,且 Owner ID 不是自己。
  3. 线程 B 通过 CAS (Compare And Swap) 尝试将 Mark Word 的线程 ID 指向自己。
  4. CAS 失败(因为线程 A 仍持有偏向锁)。
  5. 线程 B 发起 偏向锁撤销请求,到达全局安全点。

3.3 调用 wait/notify 方法

偏向锁机制不支持 wait()notify()

调用 任一同步方法(如 wait())会导致偏向锁立即失效,膨胀为重量级锁(因为 wait 必须依赖 Monitor 对象),在某些实现中可能先过渡到轻量级锁,但在涉及到等待/通知机制时,通常直接升级至重量级锁。


4. 偏向锁撤销与升级的底层流程

当出现竞争(如上述 3.2 节所述),JVM 必须介入处理。这是一个“昂贵”的操作,因为它涉及全局安全点。

graph LR A["Start: Thread A Holds Bias"] --> B{Thread B Tries Lock?} B -- No --> A B -- Yes --> C["CAS Attempt Fails"] C --> D["Request Safepoint"] D --> E["Reach Safepoint (STW)"] E --> F{Is Thread A Alive?} F -- "No / Not on Stack" --> G["Revoke to Anonymous Bias"] F -- "Yes / Holding Lock" --> H["Revoke & Bulk Rebias"] G --> I["Mark Word: Lightweight (00)"] H --> I I --> J["Thread B CAS Acquires Lightweight"]

注意:流程图中包含特殊字符与空格,已在代码块中严格按照双引号包裹规范编写。

4.1 撤销开销详解

到达 全局安全点是最大的性能开销点。

  1. 暂停所有线程:为了安全地修改 Mark Word,JVM 必须让所有运行中的线程到达安全点并暂停。
  2. 检查持有者栈帧:JVM 遍历 偏向锁持有者(线程 A)的栈帧,检查 该对象是否还被锁定。
  3. 批量重偏向或撤销:如果 JVM 发现 同一个类的多个对象都被撤销,它会触发“批量重偏向”或“批量撤销”策略,以减少后续的撤销开销。

5. 代码实战:观察状态变化

通过一段代码演示从偏向锁升级到轻量级锁(或撤销)的过程。

编写 测试类 LockEscalationDemo

import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;

public class LockEscalationDemo {
    public static void main(String[] args) throws InterruptedException {
        // 延迟 5 秒确保 JVM 启动完成(JVM 启动初期会有大量操作,可能导致偏向锁延迟开启)
        Thread.sleep(5000);

        Object obj = new Object();

        // 1. 打印无锁状态
        out.println("1. Before Lock:");
        out.println(ClassLayout.parseInstance(obj).toPrintable());

        // 2. 主线程加锁(偏向主线程)
        synchronized (obj) {
            out.println("2. Main Thread Locked (Biased):");
            out.println(ClassLayout.parseInstance(obj).toPrintable());
        }

        // 3. 这里为了演示撤销,我们模拟hashCode调用
        // 调用 hashCode 会导致偏向锁撤销,变为无锁(含 hashcode)或不可偏向
        obj.hashCode();
        out.println("3. After hashCode (Biased Revoked):");
        out.println(ClassLayout.parseInstance(obj).toPrintable());

        // 4. 新线程竞争
        Thread t = new Thread(() -> {
            synchronized (obj) {
                out.println("4. Other Thread Locked (Lightweight):");
                out.println(ClassLayout.parseInstance(obj).toPrintable());
            }
        });
        t.start();
        t.join();
    }
}

观察 输出结果的 java.lang.Object object header 行:

  • 步骤 1:应看到 ...0001(无锁,Biased Lock: 0)。
  • 步骤 2:应看到 ...0101(偏向锁,Biased Lock: 1),且包含当前线程的 ID。
  • 步骤 3:若偏向锁因 HashCode 被撤销,通常会变为 ...0001(无锁)且包含 HashCode 值,或者根据 JVM 版本策略直接标记为不可偏向。
  • 步骤 4:由于步骤 3 撤销了偏向锁,新线程加锁将直接通过 CAS 尝试,进入轻量级锁状态 ...0100(Lock: 00)。

6. 核心结论总结

在实际开发中,应根据锁的竞争情况调整策略。

  • 偏向锁:适用于几乎不存在竞争的单线程场景。撤销开销大(需要 STW),一旦有多线程竞争,弊大于利。
  • 轻量级锁:适用于交替执行的线程(无激烈竞争)。升级过程由 CAS 指令完成,不需要进入内核态,开销较小。
  • 撤销升级点:只要出现跨线程访问、调用 hashCode 或调用 wait/notify,偏向锁就会退出舞台。

配置 生产环境参数时,若系统锁竞争激烈,建议直接关闭偏向锁:

-XX:-UseBiasedLocking

这可以避免偏向锁撤销带来的 STW 开销,让锁直接进入轻量级锁逻辑。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文