文章目录

Java 锁升级过程:偏向锁到轻量级锁到重量级锁

发布于 2026-04-03 05:09:24 · 浏览 12 次 · 评论 0 条

Java 锁升级过程:偏向锁到轻量级锁到重量级锁

Java 虚拟机(JVM)为了提升多线程环境下对象同步的性能,设计了一套锁升级机制。这套机制从最轻量的“偏向锁”开始,在竞争加剧时逐步升级为“轻量级锁”,最终在高竞争场景下转为“重量级锁”。整个过程对开发者透明,但理解其原理有助于写出更高效的并发代码。


什么是锁?为什么需要升级?

在 Java 中,synchronized 关键字用于实现线程同步。早期 JVM 直接依赖操作系统的互斥量(Mutex)实现,这种锁被称为“重量级锁”,因为每次获取或释放都需要用户态与内核态切换,开销很大。

为减少不必要的性能损耗,JVM 引入了锁升级策略:只有在真正发生竞争时,才使用更重的锁机制。若一个对象长期只被一个线程访问,则无需复杂同步逻辑。


第一步:偏向锁(Biased Locking)

偏向锁的核心思想是:假设未来还会由同一个线程加锁,因此直接“偏向”它,免去后续同步开销

  1. 当一个线程首次执行 synchronized 代码块时,JVM 检查对象头中的 Mark Word。
  2. 如果 Mark Word 处于“可偏向”状态(即未锁定且未被其他线程偏向),JVM 将当前线程 ID 记录到 Mark Word 中,并将锁标志位设为“偏向锁”。
  3. 此后该线程再次进入同一 synchronized 块时,只需比对 Mark Word 中的线程 ID 是否匹配。若匹配,直接执行临界区代码,无需任何 CAS(Compare-And-Swap)操作或系统调用

注意:偏向锁默认在 JVM 启动后延迟 4 秒才启用(可通过 -XX:BiasedLockingStartupDelay=0 禁用延迟)。某些场景(如大量使用 synchronized 但无竞争)可受益;但在高并发或频繁线程切换场景下,偏向锁反而增加撤销成本,可通过 -XX:-UseBiasedLocking 完全关闭。


第二步:轻量级锁(Lightweight Locking)

当第二个线程尝试获取已被偏向的对象锁时,偏向锁失效,升级为轻量级锁

  1. 第二个线程发现对象已被其他线程偏向,于是发起“撤销偏向”(Bias Revocation)。
  2. JVM 暂停拥有偏向锁的线程(安全点 Safepoint),检查该线程是否仍在执行同步块:
    • 若已退出,则直接将锁转为“无锁”状态,新线程重新竞争。
    • 若仍在执行,则将锁升级为轻量级锁
  3. 轻量级锁通过 CAS 操作实现
    • 每个线程在栈帧中创建一个 Lock Record。
    • 尝试用 CAS 将对象头的 Mark Word 替换为指向自己 Lock Record 的指针。
    • 若 CAS 成功,获得锁;若失败,说明有竞争,继续自旋重试(默认最多 10 次,由 -XX:PreBlockSpin 控制)。

轻量级锁适用于短时间、低竞争的同步场景。自旋避免了线程挂起/唤醒的开销,但如果自旋多次仍未获得锁,就会进一步升级。


第三步:重量级锁(Heavyweight Locking)

当轻量级锁自旋超过阈值,或多个线程同时竞争时,锁升级为重量级锁

  1. 竞争线程在自旋一定次数后仍未获得锁,JVM 将其挂起(阻塞),并进入操作系统等待队列。
  2. 对象头中的 Mark Word 被替换为指向 Monitor 对象的指针(Monitor 是 JVM 内部用于实现重量级锁的数据结构)。
  3. Monitor 包含一个 _owner 字段(记录持有锁的线程)、一个 _EntryList(阻塞队列)和一个 _WaitSet(调用 wait() 的线程集合)
  4. 后续所有线程请求该锁时,都会被放入 _EntryList,由操作系统调度器决定何时唤醒。

重量级锁的代价最高,但能保证在高并发下的公平性和稳定性。


锁状态在对象头中的表示

Java 对象在内存中的布局包含“对象头”,其中 Mark Word 存储锁信息。不同锁状态下,Mark Word 的内容如下:

锁状态 Mark Word 最后三位(64位 JVM) 存储内容
无锁 001 对象哈希码、分代年龄等
偏向锁 101 线程 ID、Epoch、分代年龄等
轻量级锁 000 指向线程栈中 Lock Record 的指针
重量级锁 010 指向 Monitor 对象的指针
GC 标记 111

注:实际位模式可能因 JVM 版本略有差异,但逻辑一致。


锁升级的不可逆性

锁升级是单向的:偏向锁 → 轻量级锁 → 重量级锁,不可降级

  • 一旦升级为重量级锁,即使后续无竞争,也不会退回轻量级或偏向状态。
  • 这是因为 Monitor 结构一旦分配,就难以安全回收;而偏向锁撤销本身也有成本。

因此,在高并发服务中,若已知存在多线程竞争,建议关闭偏向锁以避免不必要的撤销开销。


如何验证锁状态?

可通过以下方式观察锁升级过程:

  1. 编写测试代码

    public class LockDemo {
     private static final Object lock = new Object();
    
     public static void main(String[] args) throws InterruptedException {
         // 单线程:应为偏向锁
         synchronized (lock) {
             System.out.println("First thread holding lock");
         }
    
         Thread.sleep(5000); // 等待偏向锁生效
    
         // 启动第二个线程触发升级
         Thread t1 = new Thread(() -> {
             synchronized (lock) {
                 try { Thread.sleep(1000); }
                 catch (InterruptedException e) { e.printStackTrace(); }
             }
         });
    
         t1.start();
         t1.join();
     }
    }
  2. 启动 JVM 时添加参数

    -XX:+PrintAssembly
    -XX:+TraceClassLoading
    -XX:+PrintBiasedLockingStatistics

    或使用 jol(Java Object Layout)工具打印对象头:

    java -jar jol-cli/target/jol-cli.jar internals LockDemo

性能建议

  1. 避免不必要的同步:缩小 synchronized 块范围,减少锁持有时间。
  2. 高并发场景禁用偏向锁:添加 JVM 参数 -XX:-UseBiasedLocking
  3. 考虑使用 java.util.concurrent 包中的工具类(如 ReentrantLockAtomicInteger),它们基于 CAS 和队列算法,通常比 synchronized 更灵活高效。
  4. 不要依赖锁升级细节编写业务逻辑:这是 JVM 内部优化机制,行为可能随版本变化。

锁升级流程总结

graph TD A["对象创建: 无锁状态"] --> B{"第一个线程\n尝试加锁?"} B -- "是" --> C["设置偏向锁:\n记录线程ID"] C --> D{"同一线程\n再次加锁?"} D -- "是" --> E["直接执行:\n无需同步"] D -- "否" --> F{"其他线程\n尝试加锁?"} F -- "是" --> G["撤销偏向锁"] G --> H{"原线程是否\n仍在同步块?"} H -- "否" --> I["转为无锁,\n新线程重试"] H -- "是" --> J["升级为轻量级锁"] J --> K{"CAS获取锁\n成功?"} K -- "是" --> L["执行临界区"] K -- "否" --> M["自旋重试\n(默认10次)"] M --> N{"自旋超限\n或更多竞争?"} N -- "是" --> O["升级为重量级锁:\n挂起线程"] N -- "否" --> K L --> P["释放锁"] O --> P P --> Q["回到无锁或\n保持重量级"]

评论 (0)

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

扫一扫,手机查看

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