Java 偏向锁(Biased Locking)详解

阿里云教程13小时前发布
2 2 0

Java 6 引入了偏向锁(Biased Locking),旨在显著降低早期版本中 synchronized锁在无竞争场景下的性能开销;到了 Java 15,偏向锁默认被禁用;最终在 Java 17 中被彻底移除。尽管已退出历史舞台,其“针对常见场景极致优化”的设计思想仍具有重大的借鉴价值。


一、背景

1.1 synchronized 的性能痛点

Java 早期版本(JDK 1.5 之前),synchronized 关键字直接映射到操作系统互斥量(Mutex),每次加锁/解锁都涉及用户态 ↔ 内核态切换,开销巨大(数百纳秒至微秒级)。即使单线程反复进入同步块,也需执行昂贵的原子操作开销。

1.2 锁优化的演进路径

为提升性能,HotSpot JVM 引入了多级锁升级机制

Java 偏向锁(Biased Locking)详解

其中:

  • 偏向锁:针对单线程重复加锁场景;
  • 轻量级锁:针对多线程交替执行(无竞争) 场景;
  • 重量级锁:存在真实竞争时,退回到 OS Mutex。

✅ 中心思想“大多数锁不存在竞争”(观察表明 >90% 的锁仅被单线程持有)。偏向锁正是为这类场景设计的极致优化。


二、Java 对象内存布局与 Mark Word

要理解偏向锁,必须要理解 Java 对象在内存中的结构,尤其是对象头(Object Header)

2.1 对象内存布局(64 位 JVM,开启指针压缩)

组成

大小

说明

Mark Word

8 字节(64 bits)

存储哈希码、GC 分代年龄、锁状态等

Klass Pointer

4 字节(32 bits)

指向类元数据(开启 -XX:+UseCompressedOops)

Instance Data

可变

实例字段

Padding

可变

对齐填充

聚焦 Mark Word —— 锁状态的核心载体。

2.2 Mark Word 结构(动态变化)

Mark Word 的内容随锁状态动态重解释(overlaid layout):

锁状态

Mark Word 布局(64 bits)

说明

无锁(Normal)

unused:25 identity_hashcode:31 unused:1 age:4 biased_lock:1(0) lock:2(01)

存储 hashCode

匿名偏向(Anonymously Biased)

unused:25 zeros:31 unused:1 age:4 biased_lock:1(1) lock:2(01)

可偏向但未绑定线程

偏向锁(Biased)

thread:54 epoch:2 age:4 biased_lock:1(1) lock:2(01)

已偏向某线程

轻量级锁

ptr_to_lock_record:62 lock:2(00)

指向栈中 Lock Record

重量级锁

ptr_to_monitor:62 lock:2(10)

指向 ObjectMonitor

GC 标记

… lock:2(11)

GC 使用

关键标志位

biased_lock:1 表明支持偏向

lock:末两位决定锁状态

01:无锁或偏向锁

00:轻量级锁

10:重量级锁

11:GC 标记

重大限制:一旦对象的 identity hash code 被计算(如调用 System.identityHashCode(obj)),JVM 会将 hashCode 写入 Mark Word,永久失去偏向能力(因空间被占用)。


三、偏向锁工作机制

3.1 偏向锁的生命周期

Java 偏向锁(Biased Locking)详解

3.2 详细流程解析

1:对象初始化(Unlocked Normal)

  • 对象刚创建,Mark Word 处于 无锁状态
  • biased_lock=0, lock=01;
  • 此时若计算 hashCode,则永久锁定在此状态。

2:进入可偏向状态(Anonymously Biased)

  • JVM 启动后 默认延迟 4 秒(-XX:BiasedLockingStartupDelay=4000)才激活偏向锁;
  • 所有新创建对象(未计算 hashCode)的 Mark Word 被重置为:[0…0 (25) | 0…0 (31) | 0 | age:0 | biased_lock:1 | lock:01]
  • 此状态称为 “匿名偏向” —— 可被任意线程偏向,但尚未绑定。

为何延迟?避免 JVM 启动阶段大量临时对象被偏向,随后又被多线程竞争,导致频繁撤销。

3:首次加锁(Acquire Bias)

线程 T1 执行 synchronized(obj):

  1. 检查 Mark Word:
  2. biased_lock == 1 ✅
  3. lock == 01 ✅
  4. thread == 0(匿名偏向)✅
  5. 构造新的 Mark Word 值:
  6. thread = T1.id(54 位线程 ID)
  7. epoch = 当前 epoch(用于批量撤销)
  8. 保留 age, biased_lock=1, lock=01
  9. 执行 CAS 替换 Mark Word;
  10. 成功 → 对象目前偏向 T1;
  11. 失败 → 可能有并发竞争,进入 撤销流程

关键点:这是唯一一次原子操作。后续 T1 加锁无需任何 CAS!

4:同线程重复加锁(Zero-Cost Entry)

T1 再次执行 synchronized(obj):

  1. 读取 Mark Word;
  2. 比较 thread == T1.id;
  3. 相等 → 直接进入临界区,无任何同步指令!

性能优势:比无锁还快(因省去了 volatile 读的内存屏障)。

5:其他线程竞争(Bias Revocation)

线程 T2 尝试获取已被 T1 偏向的锁:

  1. 发现 thread ≠ T2.id;
  2. 触发 偏向锁撤销(Bias Revocation)
  3. JVM 请求 安全点(Safepoint),暂停所有 Java 线程;
  4. 检查 T1 的状态:
  5. 撤销偏向,升级为轻量级锁
  6. T1 的 Lock Record 被初始化;
  7. T2 自旋尝试获取轻量级锁。
  8. 将锁重置为 匿名偏向 或直接 偏向 T2
  9. T2 通过 CAS 获取锁。
  10. 情况 A:T1 已退出所有同步块
  11. 情况 B:T1 仍在某个同步块中
  12. 恢复线程执行。

⚠️ 代价STW(Stop-The-World)!这是偏向锁最大的缺点。

6:批量撤销与重偏向(Bulk Revocation & Rebias)

为避免频繁单个撤销,JVM 引入 epoch 机制

  • 每个类维护一个 epoch 值;
  • 每次发生偏向撤销时,递增该类的 epoch
  • 对象 Mark Word 中也存储 epoch;
  • 当线程发现 对象.epoch ≠ 类.epoch,说明该类的偏向已被批量撤销;
  • 此时可安全地重新偏向当前线程,无需 STW。

目的:减少 STW 次数,提升高竞争场景下的恢复效率。


四、偏向锁的配置与控制

4.1 JVM 参数

参数

默认值

说明

-XX:+UseBiasedLocking

true(JDK 14-)

启用偏向锁

-XX:BiasedLockingStartupDelay=4000

4000 ms

JVM 启动后延迟激活偏向锁

-XX:+BiasedLockingStartupDelay=0

立即启用(测试用)

-XX:-UseBiasedLocking

强制禁用

(推荐 JDK 15+)

4.2 JDK 版本演进

JDK 版本

偏向锁状态

说明

JDK 6–14

默认启用

主流使用期

JDK 15

默认禁用

JEP 374:Disable and Deprecate

JDK 17

完全移除

代码从 HotSpot 中删除

移除缘由

现代应用多为高并发,偏向锁收益低;

撤销开销(STW)在容器化/低延迟场景不可接受;

Project Loom(虚拟线程)使偏向锁难以维护。


五、适用场景与反模式

✅ 适合使用偏向锁的场景

  1. 单线程长期持有锁
  2. GUI 应用(Swing/AWT 组件)
  3. 单线程处理消息队列
  4. 对象生命周期内仅被一个线程访问
  5. ThreadLocal 对象
  6. 线程私有缓存
  7. 锁竞争极少发生
  8. 配置对象、只读数据结构

❌ 不适合的场景(应禁用)

  1. 高并发、多线程竞争同一锁
  2. Web 服务器的共享缓存
  3. 生产者-消费者队列
  4. 使用 java.util.concurrent 包
  5. ReentrantLock、ConcurrentHashMap 等不走 JVM 锁体系
  6. 显式调用 System.identityHashCode()
  7. 导致对象无法进入偏向状态
  8. JDK 15+ 环境
  9. 默认禁用,强行启用无意义

六、实验验证与工具

6.1 使用 JOL 查看对象头

// Maven: org.openjdk.jol:jol-core
public class BiasedLockTest {
    static final Object obj = new Object();

    public static void main(String[] args) throws Exception {
        Thread.sleep(5000); // 等待偏向锁激活

        // 无锁状态
        System.out.println("1. 初始: " + ClassLayout.parseInstance(obj).toPrintable());

        synchronized (obj) {
            // 偏向锁状态
            System.out.println("2. 偏向后: " + ClassLayout.parseInstance(obj).toPrintable());
        }

        // 计算 hashCode
        obj.hashCode();
        System.out.println("3. hashCode 后: " + ClassLayout.parseInstance(obj).toPrintable());
    }
}

输出解读(64 位 JVM):

  • 初始:0x0000000000000005 → 末三位 101 = 匿名偏向
  • 偏向后:0x00007f8b1c000005 → 高 54 位为线程 ID
  • hashCode 后:0x00000007a0d00e05 → 高 31 位为 hashCode,无法再偏向

6.2 使用 JFR 监控锁事件

启动录制:

java -XX:StartFlightRecording=settings=profile,duration=30s,filename=lock.jfr BiasedLockTest

JDK Mission Control (JMC) 中查看:

  • Java Monitor Blocked 事件
  • Thread Contention 时间线
  • 可观察到偏向锁撤销时的 STW 停顿

七、常见问题

Q1:偏向锁真的“零开销”吗?

  • 加锁时:是的,仅一次比较(if (mark.thread == current));
  • 但有隐含成本
    • 对象头占用更多缓存行(因存储线程 ID);
    • 撤销时的 STW 开销可能远超收益;
    • 在 NUMA 架构下,跨 CPU 访问线程 ID 可能变慢。

Q2:为什么轻量级锁不用“偏向”思想?

  • 轻量级锁依赖 Lock Record(位于线程栈),天然绑定线程;
  • 但每次加锁仍需 CAS 更新 Mark Word 为指向 Lock Record 的指针;
  • 偏向锁更进一步:连指针都不存,直接存线程 ID

Q3:synchronized 和 ReentrantLock 的锁优化有何不同?

特性

synchronized

ReentrantLock

偏向锁

✅ 支持

❌ 不支持

轻量级锁

✅ JVM 优化

❌ 基于 AQS

锁消除/粗化

✅ JIT 优化

❌ 无

可中断/超时

公平锁

synchronized 在无竞争单线程场景性能更优;ReentrantLock 在复杂并发控制场景更灵活。


八、总结

维度

说明

目标

优化“无竞争单线程”场景

核心机制

将锁永久分配给第一个获取者

性能收益

后续加锁零成本(仅一次比较)

主要代价

撤销需 STW,高并发下得不偿失

历史地位

JVM 锁优化的重大里程碑

现状

已退出主流(JDK 17+ 移除)

偏向锁体现了 “针对常见 case 极致优化” 的工程思想。虽然不再适用于现代高并发应用,但其设计思路(如 epoch 批量撤销)仍影响着 JVM 的其他子系统。

不知道从什么时候开始,在每个东西上面都有一个日子,秋刀鱼会过期,肉酱也会过期,连保鲜纸都会过期。我开始怀疑,在这个世界上,还有什么东西是不会过期的?” –《重庆森林》

存在过或许就是有些事物出现的最大价值。

© 版权声明

相关文章

2 条评论

none
暂无评论...