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() 会强制撤销偏向锁:
- JVM 检查 对象是否处于偏向状态。
- 若处于偏向状态,JVM 撤销 偏向锁。
- JVM 恢复 对象为无锁状态(或轻量级锁,取决于后续操作),并将 HashCode 写入
Mark Word。
3.2 其它线程竞争锁
这是最常见的升级场景。线程 A 持有偏向锁,线程 B 尝试获取同一个锁。
分析 竞争时的执行流程:
- 线程 B 访问 对象头。
- 线程 B 发现 对象处于偏向状态,且 Owner ID 不是自己。
- 线程 B 通过 CAS (Compare And Swap) 尝试将
Mark Word的线程 ID 指向自己。 - CAS 失败(因为线程 A 仍持有偏向锁)。
- 线程 B 发起 偏向锁撤销请求,到达全局安全点。
3.3 调用 wait/notify 方法
偏向锁机制不支持 wait() 和 notify()。
调用 任一同步方法(如 wait())会导致偏向锁立即失效,膨胀为重量级锁(因为 wait 必须依赖 Monitor 对象),在某些实现中可能先过渡到轻量级锁,但在涉及到等待/通知机制时,通常直接升级至重量级锁。
4. 偏向锁撤销与升级的底层流程
当出现竞争(如上述 3.2 节所述),JVM 必须介入处理。这是一个“昂贵”的操作,因为它涉及全局安全点。
注意:流程图中包含特殊字符与空格,已在代码块中严格按照双引号包裹规范编写。
4.1 撤销开销详解
到达 全局安全点是最大的性能开销点。
- 暂停所有线程:为了安全地修改
Mark Word,JVM 必须让所有运行中的线程到达安全点并暂停。 - 检查持有者栈帧:JVM 遍历 偏向锁持有者(线程 A)的栈帧,检查 该对象是否还被锁定。
- 批量重偏向或撤销:如果 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 开销,让锁直接进入轻量级锁逻辑。

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