锁升级

663 阅读17分钟

锁升级


偏向锁

Java 偏向锁的加锁流程详解

偏向锁(Biased Locking)是 JVM 针对单线程重复访问同步代码块的一种优化机制,核心目标是减少无竞争情况下的同步开销。以下是偏向锁的加锁流程分步解析:

1. 初始状态:对象创建
  • Mark Word 结构 当对象首次被创建时(假设 JVM 启用了偏向锁),对象头的 Mark Word 会被初始化为 可偏向状态

    • 偏向模式位(Biased Lock Flag):1(表示支持偏向锁)
    • 锁标志位(Lock Flag):01(表示无锁或偏向锁)
    • 线程 ID:未关联任何线程(初始为空)。
  | 偏向模式位 (1) | 锁标志位 (01) | 其他字段(分代年龄、哈希码等) |
2. 首次加锁:线程尝试获取锁

当第一个线程(假设为 线程 A)尝试获取偏向锁时,流程如下:

  1. 检查 Mark Word 状态 JVM 检查对象头的 Mark Word

    • 偏向模式位是否为 1(可偏向)?
    • 线程 ID 是否为空(未关联任何线程)?
  2. CAS 操作写入线程 ID

    • 若上述条件满足,JVM 通过 CAS 操作 尝试将当前线程(线程 A)的 ID 写入 Mark Word 的线程 ID 字段。

    • CAS 成功

      • 对象进入 偏向锁状态,线程 A 直接进入同步代码块。
      • Mark Word 更新为:
       | 1 (偏向模式) | 01 (锁标志位) | 线程 A 的 ID | 其他字段 |
  • CAS 失败

    • 说明存在竞争(其他线程已抢先修改了 Mark Word),触发 偏向锁撤销(Revoke Bias),升级为轻量级锁。

3. 重入加锁:同一线程再次获取锁

当线程 A 再次进入同一同步代码块时:

  1. 检查线程 ID 一致性 JVM 检查 Mark Word 中的线程 ID 是否与当前线程(线程 A)一致。

  2. 直接访问同步代码

    • 若一致,无需任何原子操作(如 CAS),直接允许线程 A 进入同步代码块。
    • 这是偏向锁的核心优势:单线程重入时完全无额外开销

4. 竞争加锁:其他线程尝试获取锁

当另一个线程(线程 B)尝试获取已被线程 A 偏向的锁时:

  1. 触发偏向锁撤销

    • JVM 发现 Mark Word 中的线程 ID 不是线程 B。
    • 暂停所有线程(进入 全局安全点,即 Safe Point),准备撤销偏向锁。
  2. 撤销流程

    • 检查线程 A 是否存活

      • 若线程 A 已终止:直接重置 Mark Word 为无锁状态(偏向模式位 0,锁标志位 01)。
      • 若线程 A 仍在同步块中:将锁升级为 轻量级锁
    • 轻量级锁升级

      1. Mark Word 替换为指向线程 A 栈中锁记录(Lock Record)的指针。
      2. 锁标志位更新为 00(轻量级锁状态)。
  3. 恢复竞争

    • 唤醒所有线程,线程 B 通过 轻量级锁机制(CAS 自旋)尝试获取锁。

5. 流程图总结
首次加锁:
1. 检查 Mark Word → 可偏向且无线程 ID?
   ├─ 是 → CAS 写入线程 ID → 进入同步块
   └─ 否 → 触发偏向锁撤销
​
重入加锁:
1. 检查线程 ID → 一致?
   ├─ 是 → 直接进入同步块
   └─ 否 → 触发偏向锁撤销
​
竞争加锁:
1. 触发撤销 → 暂停线程(安全点)
   ├─ 原线程存活且在同步块 → 升级为轻量级锁
   └─ 原线程未存活或退出 → 重置为无锁状态

关键机制解析

1. CAS 操作的作用
  • 原子性保证:通过 CAS 确保只有一个线程能成功写入自己的 ID,避免并发冲突。
  • 失败处理:CAS 失败表明竞争存在,需撤销偏向锁以升级锁机制。
2. 全局安全点(Safe Point)
  • 必要性:撤销偏向锁需修改 Mark Word 和线程栈,必须暂停所有线程以避免数据不一致。
  • 性能影响:频繁触发安全点可能导致短暂 STW(Stop-The-World),高竞争场景需谨慎。
3. 锁升级策略
  • 偏向锁 → 轻量级锁 → 重量级锁:根据竞争强度逐步升级,平衡性能与开销。
  • 轻量级锁的 CAS 自旋:适用于低竞争场景,避免直接进入重量级锁的高开销。

性能特点与适用场景

优点
  • 单线程极致优化:同一线程重复获取锁时无任何原子操作。
  • 低开销:偏向锁的撤销和升级仅在竞争发生时触发。
缺点
  • 撤销成本高:全局安全点可能导致性能抖动。
  • 不适合高竞争场景:频繁竞争会退化为轻量级/重量级锁,失去优化意义。
应用建议
  • 适用场景:单线程主导的同步块(如初始化、事件循环)。
  • 禁用偏向锁:在高度多线程竞争的环境下,可通过 JVM 参数 -XX:-UseBiasedLocking 关闭。

Java 版本差异

  • Java 15+ 默认禁用偏向锁:因现代应用多线程竞争复杂,偏向锁的优化收益有限,默认关闭以减少潜在性能问题。

总结

偏向锁通过减少无竞争时的同步开销,显著优化单线程场景下的性能。其加锁流程围绕 CAS 操作线程 ID 匹配 展开,核心在于避免无竞争时的原子操作。然而,在竞争激烈时,撤销与升级流程可能带来额外开销,需根据实际场景权衡是否启用。理解这一机制有助于合理设计同步代码或调整 JVM 参数,以最大化程序性能。

轻量级锁


轻量级锁的加锁与解锁流程详解

轻量级锁(Lightweight Locking)是 JVM 针对 低竞争多线程场景 的优化机制,通过 CAS 自旋栈帧锁记录 减少同步开销。其核心思想是避免直接使用重量级锁(操作系统互斥量)的开销,适用于线程交替执行同步块但无实际竞争的场景。


一、轻量级锁的加锁流程

1. 初始状态:对象无锁或偏向锁已撤销
  • 对象头的 Mark Word 处于 无锁状态(偏向模式位 0,锁标志位 01)或 偏向锁已撤销(升级为轻量级锁前的中间状态)。
2. 加锁步骤
  1. 创建锁记录(Lock Record)

    • 线程进入同步块时,在栈帧中创建一个 锁记录,用于保存对象头原始的 Mark Word

    • 锁记录结构:

      | Displaced Mark Word (原对象头内容) | Owner (当前线程指针) | ... |
      
  2. 尝试 CAS 替换对象头

    • 通过 CAS 操作 尝试将对象头的 Mark Word 替换为指向锁记录的指针。

      原对象头 Mark Word → 复制到锁记录的 Displaced Mark Word 字段
      新对象头 Mark Word → 指向锁记录的指针(锁标志位 `00`)
      
    • CAS 成功:对象进入轻量级锁状态,线程获取锁。

    • CAS 失败:存在竞争,触发 锁膨胀(升级为重量级锁)。

  3. 处理 CAS 失败(锁膨胀)

    • 若 CAS 失败,表示其他线程正在竞争锁:

      • JVM 将锁升级为 重量级锁(锁标志位 10)。
      • 当前线程进入 阻塞状态,等待操作系统调度。

二、轻量级锁的解锁流程

1. 解锁步骤
  1. 恢复对象头 Mark Word

    • 通过 CAS 操作 尝试将对象头的 Mark Word 恢复为锁记录中保存的 Displaced Mark Word(原始状态)。
    • CAS 成功:对象回到无锁状态,解锁完成。
    • CAS 失败:说明锁已升级为重量级锁,需通过操作系统机制释放锁。
  2. 处理 CAS 失败(锁已膨胀)

    • 若 CAS 失败,表示锁已升级为重量级锁:

      • 直接调用操作系统级的 互斥量解锁 操作。
      • 唤醒等待该锁的其他线程。

三、关键机制解析

1. 锁记录的作用
  • 保存原始对象头:用于解锁时恢复对象状态。
  • 标识锁持有者:指向当前线程,支持重入和锁状态管理。
2. CAS 操作的意义
  • 轻量级锁的核心:通过用户态的 CAS 操作避免内核态切换,减少开销。
  • 自旋优化:在 CAS 失败后,线程可能短暂自旋重试,避免立即阻塞。
3. 锁膨胀(Lock Inflation)
  • 触发条件:轻量级锁 CAS 失败(存在竞争)。

  • 升级为重量级锁

    1. 对象头 Mark Word 替换为指向 重量级锁监视器(Monitor) 的指针。
    2. 未获取锁的线程进入阻塞队列,由操作系统调度。

四、流程图总结

加锁流程
1. 创建锁记录(保存原对象头)
2. CAS 替换对象头为锁记录指针
   ├─ 成功 → 进入同步块(轻量级锁)
   └─ 失败 → 升级为重量级锁(线程阻塞)
解锁流程
1. CAS 恢复对象头为原 Mark Word
   ├─ 成功 → 对象回到无锁状态
   └─ 失败 → 调用操作系统解锁(重量级锁)

五、轻量级锁 vs 偏向锁 vs 重量级锁

特性轻量级锁偏向锁重量级锁
锁记录✅ 生成(保存原对象头)❌ 不生成❌ 不生成
加锁操作CAS 替换对象头CAS 写入线程 ID(首次)依赖操作系统互斥量
解锁操作CAS 恢复对象头无操作(保持偏向状态)操作系统释放互斥量
竞争处理自旋失败后升级为重量级锁直接升级为轻量级锁线程阻塞/唤醒
适用场景低竞争多线程交替执行单线程重复访问高竞争多线程

六、性能特点与适用场景

优点
  • 低开销:CAS 操作和自旋减少线程阻塞。
  • 快速响应:无竞争时几乎无额外代价。
缺点
  • 自旋浪费 CPU:高竞争时自旋可能导致 CPU 空转。
  • 锁膨胀成本:升级为重量级锁的流程较复杂。
适用场景
  • 线程交替执行同步块:例如生产者-消费者模式中交替操作。
  • 短临界区:同步代码块执行时间短,适合自旋优化。

七、示例代码分析

// 线程 A 获取轻量级锁
synchronized (obj) {
    // 同步代码块
    // 对象头指向线程 A 的锁记录(锁标志位 00)
}
​
// 线程 B 尝试获取锁(触发 CAS 失败)
synchronized (obj) {
    // 锁升级为重量级锁(锁标志位 10)
    // 线程 B 进入阻塞队列
}

总结

轻量级锁通过 CAS 操作锁记录 在低竞争场景下优化同步性能,避免了重量级锁的开销。其核心流程围绕 CAS 替换与恢复对象头展开,在竞争升级时触发锁膨胀机制。理解这一机制有助于设计高效同步代码或调优 JVM 参数(如自旋次数 -XX:PreBlockSpin)。

重量级锁


重量级锁的加锁与解锁流程详解

重量级锁(Heavyweight Locking)是 JVM 处理高竞争多线程场景的最终锁机制,基于 操作系统互斥量(Mutex) 实现。其核心特点是 线程阻塞与唤醒依赖内核态操作,适用于多线程激烈竞争的场景,但会带来较高的性能开销。


一、重量级锁的触发条件

当轻量级锁的 CAS 自旋失败锁膨胀(Lock Inflation) 时,JVM 会将锁升级为重量级锁。具体触发场景包括:

  1. 多个线程同时竞争同一锁。
  2. 轻量级锁自旋超过阈值(默认自旋次数由 -XX:PreBlockSpin 控制)。
  3. 调用 wait(), notify() 等方法(需依赖监视器模型)。

二、重量级锁的加锁流程

1. 锁膨胀(升级为重量级锁)

当轻量级锁竞争失败时,JVM 触发锁膨胀流程:

  1. 创建监视器(Monitor)

    • 为对象分配一个 Monitor 对象(即管程,C++ 实现的结构体),存储以下信息:

      • Owner:当前持有锁的线程。
      • EntryList:阻塞等待锁的线程队列。
      • WaitSet:调用 wait() 后进入等待状态的线程队列。
      • Recursions:锁重入次数(支持同一线程多次获取锁)。
    • 对象头的 Mark Word 被替换为指向 Monitor 的指针(锁标志位 10)。

  2. 线程竞争锁

    • 线程尝试通过 CAS 操作获取 Monitor 的 Owner 字段:

      • CAS 成功:线程成为 Owner,进入同步块。
      • CAS 失败:线程进入 EntryList 队列,阻塞等待 操作系统调度。
2. 阻塞与唤醒机制
  • 线程阻塞:通过操作系统 pthread_mutex_lock 等函数挂起线程,进入内核态等待。
  • 线程唤醒:当锁释放时,JVM 通过 pthread_mutex_unlock 唤醒 EntryList 中的线程,由操作系统重新调度。

三、重量级锁的解锁流程

  1. 减少重入计数

    • 若线程多次重入锁(Recursions > 0),仅减少重入次数,不释放锁。
  2. 释放锁并唤醒线程

    • Recursions 降为 0 时:

      • 将 Monitor 的 Owner 字段置为 null
      • 从 EntryList 或 WaitSet 中唤醒一个或多个线程(具体策略依赖 JVM 实现)。
  3. 恢复线程竞争

    • 被唤醒的线程重新尝试获取锁(可能再次触发竞争)。

四、监视器(Monitor)的核心结构

重量级锁依赖的 Monitor 对象包含以下关键字段:

字段说明
Owner当前持有锁的线程,null 表示锁未被占用。
EntryList阻塞等待锁的线程队列(竞争锁失败的线程加入此队列)。
WaitSet调用 wait() 主动放弃锁的线程队列(需通过 notify()/notifyAll() 唤醒)。
Recursions锁的重入次数(同一线程多次获取锁时计数)。
cxq竞争锁的临时线程队列(某些 JVM 实现用此优化 EntryList 的公平性)。

五、重量级锁的性能特点

优点
  • 严格保证互斥性:通过操作系统级互斥量确保高竞争场景下的线程安全。
  • 支持复杂同步操作:兼容 wait()notify() 等机制。
缺点
  • 高开销:线程阻塞与唤醒涉及用户态到内核态切换,耗时约为微秒级(比 CAS 操作高 2 个数量级)。
  • 无自旋优化:线程直接阻塞,可能增加响应延迟。

六、流程图总结

加锁流程
轻量级锁 CAS 失败 → 创建 Monitor → 替换对象头为 Monitor 指针  
   ├─ CAS 竞争 Owner 成功 → 线程持有锁  
   └─ CAS 失败 → 线程加入 EntryList → 阻塞等待
解锁流程
减少 Recursions → 若为 0 → 释放 Owner → 唤醒 EntryList/WaitSet 线程  
   ├─ 唤醒线程重新竞争锁  
   └─ 锁保持重量级状态(通常不降级)

七、与其他锁的对比

特性重量级锁轻量级锁偏向锁
实现依赖操作系统互斥量CAS + 栈帧锁记录对象头线程 ID
线程阻塞✅ 内核态阻塞❌ 自旋(用户态)❌ 无操作
竞争处理队列管理 + 阻塞唤醒自旋失败后升级直接升级为轻量级锁
适用场景高竞争多线程低竞争多线程交替执行单线程重复访问
性能开销高(微秒级)中(纳秒级 CAS)极低(无原子操作)

八、优化措施

尽管重量级锁开销较大,JVM 仍提供了一些优化手段:

  1. 适应性自旋(Adaptive Spinning)

    • 根据历史竞争情况动态调整自旋次数(通过 -XX:+UseSpinning 启用)。
  2. 锁粗化(Lock Coarsening)

    • 合并多个相邻同步块,减少锁竞争次数。
  3. 锁消除(Lock Elision)

    • 通过逃逸分析移除不必要的锁(如局部对象锁)。

九、总结

重量级锁通过操作系统互斥量解决高竞争场景的线程同步问题,其核心流程包括 锁膨胀、线程阻塞与唤醒。虽然保证了严格的线程安全,但性能开销较大,需结合业务场景谨慎使用。理解其机制有助于:

  1. 避免不必要的锁竞争(如减少同步块粒度)。
  2. 分析多线程性能瓶颈(如通过 jstack 查看线程阻塞状态)。
  3. 合理选择同步策略(如使用 ReentrantLock 替代隐式锁)。

轻量级锁 vs 偏向锁 vs 重量级锁

特性轻量级锁偏向锁重量级锁
锁记录✅ 生成(保存原对象头)❌ 不生成❌ 不生成
加锁操作CAS 替换对象头CAS 写入线程 ID(首次)依赖操作系统互斥量
解锁操作CAS 恢复对象头无操作(保持偏向状态)操作系统释放互斥量
竞争处理自旋失败后升级为重量级锁直接升级为轻量级锁(或撤销)线程阻塞/唤醒
适用场景低竞争多线程交替执行单线程重复访问高竞争多线程

锁升级


锁升级流程详解(含触发场景与设计动机分析)


一、锁升级流程与触发场景

1. 完整锁升级流程
单线程首次访问
其他线程竞争/hashCode调用
升级
自旋失败/竞争激烈
锁释放
无锁状态
偏向锁
撤销偏向锁
轻量级锁不可逆
重量级锁
无锁 → 单线程首次访问 → 偏向锁
   │
   ├─ 其他线程竞争 → 撤销偏向锁 → 轻量级锁 → 自旋失败 → 重量级锁
   └─ 调用 hashCode → 撤销偏向锁 → 轻量级锁

2. 触发场景与具体原因
(1) 无锁 → 偏向锁
  • 触发场景

    • 单线程首次进入同步块。
    • JVM 启用偏向锁(默认 Java 15 前启用)。
  • 原因分析

    • 优化单线程性能:偏向锁通过消除 CAS 操作,减少单线程重复访问同步块的开销。
    • 设计代价极低:仅需一次 CAS 写入线程 ID,后续无原子操作。

(2) 偏向锁 → 轻量级锁
  • 触发场景

    • 其他线程竞争:第二个线程尝试获取已被偏向的锁。
    • 调用 hashCode:偏向锁的 Mark Word 无法存储哈希码,必须撤销。
  • 原因分析

    • 应对低竞争场景:轻量级锁通过 CAS 自旋减少线程阻塞。
    • 避免过早重量级锁:轻量级锁在用户态完成竞争,避免内核态切换。

(3) 轻量级锁 → 重量级锁
  • 触发场景

    • 自旋失败:轻量级锁 CAS 竞争超过阈值(默认 10 次)。
    • 调用 wait/notify:这些方法依赖监视器(Monitor)。
  • 原因分析

    • 严格保证线程安全:重量级锁通过操作系统互斥量确保高竞争下的互斥性。
    • 支持复杂同步操作:监视器提供 EntryList/WaitSet 队列管理。

二、Java 为何设计锁升级?

1. 核心目标:平衡性能与线程安全
场景锁类型性能开销适用性
无竞争偏向锁零开销单线程重复访问(如事件循环)
低竞争轻量级锁低开销线程交替执行(如生产者-消费者)
高竞争重量级锁高开销多线程频繁争抢(如秒杀场景)
  • 按需升级:JVM 根据实际竞争强度动态选择锁机制,避免一刀切使用重量级锁。

2. 锁升级的收益与代价
设计选择收益代价
偏向锁单线程零开销撤销时需要全局安全点(STW)
轻量级锁用户态自旋减少内核切换自旋浪费 CPU(高竞争时性能下降)
重量级锁严格互斥,支持复杂同步操作线程阻塞/唤醒开销大(微秒级延迟)

三、锁升级的意义

  1. 精细化性能优化

    • 90% 的同步块在无竞争或低竞争场景下运行,偏向锁和轻量级锁可大幅降低开销。
  2. 渐进式安全保证

    • 从无锁到重量级锁逐步升级,确保在不同竞争强度下均能安全运行。
  3. 兼容性设计

    • 支持 hashCode()wait() 等基础方法,避免功能缺失。

四、锁升级的典型问题

1. 偏向锁的争议
  • Java 15+ 默认禁用:现代应用多线程竞争复杂,偏向锁的撤销成本可能超过收益。

  • 手动调优建议

    # 启用偏向锁(需权衡场景)
    -XX:+UseBiasedLocking  
    # 设置批量重偏向阈值
    -XX:BiasedLockingBulkRebiasThreshold=20
    
2. 重量级锁的性能陷阱
  • 阻塞代价高:可通过减小锁粒度或使用无锁数据结构(如 ConcurrentHashMap)规避。

  • 监控工具

    • jstack:查看线程阻塞状态。
    • JFR(Java Flight Recorder):分析锁竞争热点。

五、总结

锁升级是 JVM 在多线程编程中 权衡性能与安全 的经典设计:

  • 触发场景:从单线程访问到高竞争逐步升级。
  • 设计动机:通过渐进式优化,覆盖从极致性能到严格安全的全场景需求。
  • 实际应用:理解锁升级机制可帮助开发者编写高效同步代码,避免性能反模式。