当前位置: 首页 > article >正文

深入解析 synchronized 锁升级:从偏向锁到重量级锁的设计哲学

引言

在 Java 并发编程中,synchronized 是保证线程安全的核心关键字。但早期的 synchronized 因直接使用操作系统级互斥锁(Mutex Lock)而饱受性能诟病。自 Java 6 起,JVM 团队引入了 锁升级(Lock Escalation) 机制,通过 偏向锁 → 轻量级锁 → 重量级锁 的渐进式优化,实现了性能与安全的完美平衡。本文将深入剖析每个锁状态的设计思想,揭示其背后的哲学。


一、对象头与锁的物理载体

在理解锁升级前,需先了解对象内存布局中的 对象头(Object Header),它是锁状态的物理载体:

  • Mark Word(标记字段):存储哈希码、GC 分代年龄、锁状态标志等。
  • Klass Pointer(类型指针):指向类的元数据。

锁标志位(2 bits)控制锁状态:01 无锁/偏向锁,00 轻量级锁,10 重量级锁。


二、锁升级的四个阶段

1. 无锁(Lock-Free)

设计思想

  • 零竞争场景优化:当对象未被任何线程锁定时,无需任何同步机制。
  • 锁消除(Lock Elision):JIT 编译器通过逃逸分析,消除不可能存在竞争的锁。

触发条件

  • 对象刚被创建且未被任何线程访问。
  • 锁消除优化后的代码路径。

2. 偏向锁(Biased Locking)

设计思想

  • 单线程重复访问优化:假设锁始终由同一线程获得,通过记录线程 ID 避免 CAS 操作。
  • 空间换时间:在对象头中存储偏向线程 ID,消除无竞争时的同步开销。

升级流程

  1. 首次加锁:通过 CAS 将线程 ID 写入 Mark Word,锁标志位设为偏向模式(101)。
  2. 重入检查:检查 Mark Word 中的线程 ID 是否与当前线程一致,若一致则直接执行。
  3. 竞争触发升级:当其他线程尝试获取锁时,撤销偏向锁(Revoke Bias),升级为轻量级锁。

关键代码逻辑

// 偏向锁获取伪代码  
if (markWord.threadId == currentThread.id) {  
    // 直接进入同步块  
} else {  
    // 尝试 CAS 替换线程 ID  
    if (CAS(markWord, expectedThreadId, currentThread.id)) {  
        // 成功获取偏向锁  
    } else {  
        // 升级为轻量级锁  
    }  
}  

3. 轻量级锁(Lightweight Locking)

设计思想

  • 短时竞争优化:通过线程栈中的 锁记录(Lock Record) 和 CAS 自旋,避免线程阻塞。
  • 用户态解决方案:不依赖操作系统互斥锁,减少内核态切换开销。

升级流程

  1. 加锁:将对象头的 Mark Word 复制到线程栈的锁记录中(Displaced Mark Word)。
  2. CAS 竞争:尝试通过 CAS 将对象头的 Mark Word 替换为指向锁记录的指针。
  3. 自旋失败升级:若自旋超过阈值(默认 10 次,JVM 自适应调节),升级为重量级锁。

关键代码逻辑

// 轻量级锁获取伪代码  
LockRecord lr = stack.allocateLockRecord();  
lr.displacedMarkWord = markWord;  
if (CAS(object.markWord, markWord, lr.address)) {  
    // 获取轻量级锁成功  
} else {  
    // 自旋重试或升级为重量级锁  
}  

设计权衡

  • 自适应自旋(Adaptive Spinning):JVM 根据历史竞争情况动态调整自旋次数。

4. 重量级锁(Heavyweight Locking)

设计思想

  • 长时竞争优化:通过操作系统级 互斥锁(Mutex Lock) 管理线程阻塞与唤醒。
  • 终极安全屏障:确保高竞争场景下的线程安全,但伴随上下文切换开销。

升级流程

  1. 对象监视器(Monitor):JVM 为对象分配一个 Monitor(通过 ObjectMonitor 实现)。
  2. 阻塞队列:竞争失败的线程进入 EntryList 等待唤醒。
  3. 唤醒机制:锁释放时,通过 notify/notifyAll 唤醒等待线程。

Monitor 结构

// HotSpot 源码中的 ObjectMonitor  
class ObjectMonitor {  
    volatile markOop _header;      // 原始 Mark Word  
    void*   volatile _owner;       // 持有锁的线程  
    ObjectWaiter* volatile _EntryList; // 阻塞线程队列  
    ObjectWaiter* volatile _WaitSet;   // 调用 wait() 的线程队列  
    volatile int _recursions;      // 重入次数  
    // ...  
};  

设计权衡

  • 成本与安全的平衡:重量级锁的阻塞唤醒成本高,但能保证最严格的线程安全。

三、锁升级的设计哲学

1. 渐进式优化(Progressive Optimization)

  • 场景适配:根据竞争激烈程度动态选择锁机制,避免“一刀切”的性能浪费。
  • 成本分级:无竞争 → 偏向锁(零开销)、低竞争 → 轻量级锁(CAS 自旋)、高竞争 → 重量级锁(阻塞)。

2. 无逆升级(No Downgrade)

  • 单向升级:一旦升级为重量级锁,无法降级。因降级带来的复杂性远超收益。
  • 状态重置:只有对象处于无锁状态时,才能重新偏向其他线程。

3. 空间与时间的博弈

  • 偏向锁:牺牲 Mark Word 空间(存储线程 ID),换取无竞争时的极致性能。
  • 轻量级锁:通过栈帧存储原始 Mark Word,避免对象头膨胀。

4. JVM 与 OS 的分工协作

  • 用户态优化:偏向锁和轻量级锁完全在 JVM 层面实现,避免内核态切换。
  • 内核态兜底:重量级锁依赖操作系统提供的互斥锁,确保极端场景的可靠性。

四、性能对比与最佳实践

1. 性能对比

锁类型适用场景实现成本
偏向锁单线程独占1 次 CAS
轻量级锁低竞争、短时持有CAS + 自旋
重量级锁高竞争、长时持有系统调用 + 上下文切换

2. 最佳实践

  • 缩小同步块:减少锁持有时间,降低竞争概率。
  • 避免过度同步:优先使用 java.util.concurrent 工具类(如 ReentrantLock)。
  • 监控锁竞争:通过 JConsole 或 JFR 分析 Contended 锁。

五、总结

  • 偏向锁是“乐观主义”的极致,假设世界没有竞争。
  • 轻量级锁是“有限妥协”,通过自旋尝试化解短暂冲突。
  • 重量级锁是“终极防御”,以成本换安全。

http://www.kler.cn/a/561760.html

相关文章:

  • 面试之《nodejs中,网络请求时stream和json的区别》
  • 基于STM32的智能工业生产线质量检测系统
  • Transformers框架微调Qwen和DeepSeek
  • 6.6.3 SQL数据查询(一)
  • 算法题训练 ——— NC313 两个数组的交集
  • sklearn中的决策树
  • 以绘图(绘制点、直线、圆、椭圆、多段线)为例子 通过设计模式中的命令模式实现
  • ubuntu20.04 突破文件数限制
  • 结构型模式 - 装饰者模式 (Decorator Pattern)
  • 蓝桥杯好题推荐------蛇形方阵
  • 【Java】I/O 流篇 —— 缓冲流
  • git常用命令(时常更新)
  • MySQL-增删改查
  • 微信小程序网络请求与API调用:实现数据交互
  • 在Anaconda的虚拟环境中安装R,并在vscode中使用
  • ollama无法通过IP:11434访问
  • 06C语言——指针
  • elfk+zookeeper+kafka​数据流
  • 《迈向认知智能新高度:深度融合机器学习与知识图谱技术》
  • 系统定时器SysTick