锁升级
偏向锁
Java 偏向锁的加锁流程详解
偏向锁(Biased Locking)是 JVM 针对单线程重复访问同步代码块的一种优化机制,核心目标是减少无竞争情况下的同步开销。以下是偏向锁的加锁流程分步解析:
1. 初始状态:对象创建
-
Mark Word 结构 当对象首次被创建时(假设 JVM 启用了偏向锁),对象头的
Mark Word会被初始化为 可偏向状态:- 偏向模式位(Biased Lock Flag):
1(表示支持偏向锁) - 锁标志位(Lock Flag):
01(表示无锁或偏向锁) - 线程 ID:未关联任何线程(初始为空)。
- 偏向模式位(Biased Lock Flag):
| 偏向模式位 (1) | 锁标志位 (01) | 其他字段(分代年龄、哈希码等) |
2. 首次加锁:线程尝试获取锁
当第一个线程(假设为 线程 A)尝试获取偏向锁时,流程如下:
-
检查 Mark Word 状态 JVM 检查对象头的
Mark Word:- 偏向模式位是否为
1(可偏向)? - 线程 ID 是否为空(未关联任何线程)?
- 偏向模式位是否为
-
CAS 操作写入线程 ID
-
若上述条件满足,JVM 通过 CAS 操作 尝试将当前线程(线程 A)的 ID 写入
Mark Word的线程 ID 字段。 -
CAS 成功:
- 对象进入 偏向锁状态,线程 A 直接进入同步代码块。
Mark Word更新为:
-
| 1 (偏向模式) | 01 (锁标志位) | 线程 A 的 ID | 其他字段 |
-
CAS 失败:
- 说明存在竞争(其他线程已抢先修改了
Mark Word),触发 偏向锁撤销(Revoke Bias),升级为轻量级锁。
- 说明存在竞争(其他线程已抢先修改了
3. 重入加锁:同一线程再次获取锁
当线程 A 再次进入同一同步代码块时:
-
检查线程 ID 一致性 JVM 检查
Mark Word中的线程 ID 是否与当前线程(线程 A)一致。 -
直接访问同步代码
- 若一致,无需任何原子操作(如 CAS),直接允许线程 A 进入同步代码块。
- 这是偏向锁的核心优势:单线程重入时完全无额外开销。
4. 竞争加锁:其他线程尝试获取锁
当另一个线程(线程 B)尝试获取已被线程 A 偏向的锁时:
-
触发偏向锁撤销
- JVM 发现
Mark Word中的线程 ID 不是线程 B。 - 暂停所有线程(进入 全局安全点,即 Safe Point),准备撤销偏向锁。
- JVM 发现
-
撤销流程
-
检查线程 A 是否存活:
- 若线程 A 已终止:直接重置
Mark Word为无锁状态(偏向模式位0,锁标志位01)。 - 若线程 A 仍在同步块中:将锁升级为 轻量级锁。
- 若线程 A 已终止:直接重置
-
轻量级锁升级:
- 将
Mark Word替换为指向线程 A 栈中锁记录(Lock Record)的指针。 - 锁标志位更新为
00(轻量级锁状态)。
- 将
-
-
恢复竞争
- 唤醒所有线程,线程 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. 加锁步骤
-
创建锁记录(Lock Record)
-
线程进入同步块时,在栈帧中创建一个 锁记录,用于保存对象头原始的
Mark Word。 -
锁记录结构:
| Displaced Mark Word (原对象头内容) | Owner (当前线程指针) | ... |
-
-
尝试 CAS 替换对象头
-
通过 CAS 操作 尝试将对象头的
Mark Word替换为指向锁记录的指针。原对象头 Mark Word → 复制到锁记录的 Displaced Mark Word 字段 新对象头 Mark Word → 指向锁记录的指针(锁标志位 `00`) -
CAS 成功:对象进入轻量级锁状态,线程获取锁。
-
CAS 失败:存在竞争,触发 锁膨胀(升级为重量级锁)。
-
-
处理 CAS 失败(锁膨胀)
-
若 CAS 失败,表示其他线程正在竞争锁:
- JVM 将锁升级为 重量级锁(锁标志位
10)。 - 当前线程进入 阻塞状态,等待操作系统调度。
- JVM 将锁升级为 重量级锁(锁标志位
-
二、轻量级锁的解锁流程
1. 解锁步骤
-
恢复对象头 Mark Word
- 通过 CAS 操作 尝试将对象头的
Mark Word恢复为锁记录中保存的Displaced Mark Word(原始状态)。 - CAS 成功:对象回到无锁状态,解锁完成。
- CAS 失败:说明锁已升级为重量级锁,需通过操作系统机制释放锁。
- 通过 CAS 操作 尝试将对象头的
-
处理 CAS 失败(锁已膨胀)
-
若 CAS 失败,表示锁已升级为重量级锁:
- 直接调用操作系统级的 互斥量解锁 操作。
- 唤醒等待该锁的其他线程。
-
三、关键机制解析
1. 锁记录的作用
- 保存原始对象头:用于解锁时恢复对象状态。
- 标识锁持有者:指向当前线程,支持重入和锁状态管理。
2. CAS 操作的意义
- 轻量级锁的核心:通过用户态的 CAS 操作避免内核态切换,减少开销。
- 自旋优化:在 CAS 失败后,线程可能短暂自旋重试,避免立即阻塞。
3. 锁膨胀(Lock Inflation)
-
触发条件:轻量级锁 CAS 失败(存在竞争)。
-
升级为重量级锁:
- 对象头 Mark Word 替换为指向 重量级锁监视器(Monitor) 的指针。
- 未获取锁的线程进入阻塞队列,由操作系统调度。
四、流程图总结
加锁流程
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 会将锁升级为重量级锁。具体触发场景包括:
- 多个线程同时竞争同一锁。
- 轻量级锁自旋超过阈值(默认自旋次数由
-XX:PreBlockSpin控制)。 - 调用
wait(),notify()等方法(需依赖监视器模型)。
二、重量级锁的加锁流程
1. 锁膨胀(升级为重量级锁)
当轻量级锁竞争失败时,JVM 触发锁膨胀流程:
-
创建监视器(Monitor)
-
为对象分配一个 Monitor 对象(即管程,C++ 实现的结构体),存储以下信息:
- Owner:当前持有锁的线程。
- EntryList:阻塞等待锁的线程队列。
- WaitSet:调用
wait()后进入等待状态的线程队列。 - Recursions:锁重入次数(支持同一线程多次获取锁)。
-
对象头的
Mark Word被替换为指向 Monitor 的指针(锁标志位10)。
-
-
线程竞争锁
-
线程尝试通过 CAS 操作获取 Monitor 的 Owner 字段:
- CAS 成功:线程成为 Owner,进入同步块。
- CAS 失败:线程进入 EntryList 队列,阻塞等待 操作系统调度。
-
2. 阻塞与唤醒机制
- 线程阻塞:通过操作系统
pthread_mutex_lock等函数挂起线程,进入内核态等待。 - 线程唤醒:当锁释放时,JVM 通过
pthread_mutex_unlock唤醒 EntryList 中的线程,由操作系统重新调度。
三、重量级锁的解锁流程
-
减少重入计数
- 若线程多次重入锁(
Recursions > 0),仅减少重入次数,不释放锁。
- 若线程多次重入锁(
-
释放锁并唤醒线程
-
当
Recursions降为0时:- 将 Monitor 的 Owner 字段置为
null。 - 从 EntryList 或 WaitSet 中唤醒一个或多个线程(具体策略依赖 JVM 实现)。
- 将 Monitor 的 Owner 字段置为
-
-
恢复线程竞争
- 被唤醒的线程重新尝试获取锁(可能再次触发竞争)。
四、监视器(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 仍提供了一些优化手段:
-
适应性自旋(Adaptive Spinning)
- 根据历史竞争情况动态调整自旋次数(通过
-XX:+UseSpinning启用)。
- 根据历史竞争情况动态调整自旋次数(通过
-
锁粗化(Lock Coarsening)
- 合并多个相邻同步块,减少锁竞争次数。
-
锁消除(Lock Elision)
- 通过逃逸分析移除不必要的锁(如局部对象锁)。
九、总结
重量级锁通过操作系统互斥量解决高竞争场景的线程同步问题,其核心流程包括 锁膨胀、线程阻塞与唤醒。虽然保证了严格的线程安全,但性能开销较大,需结合业务场景谨慎使用。理解其机制有助于:
- 避免不必要的锁竞争(如减少同步块粒度)。
- 分析多线程性能瓶颈(如通过
jstack查看线程阻塞状态)。 - 合理选择同步策略(如使用
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(高竞争时性能下降) |
| 重量级锁 | 严格互斥,支持复杂同步操作 | 线程阻塞/唤醒开销大(微秒级延迟) |
三、锁升级的意义
-
精细化性能优化
- 90% 的同步块在无竞争或低竞争场景下运行,偏向锁和轻量级锁可大幅降低开销。
-
渐进式安全保证
- 从无锁到重量级锁逐步升级,确保在不同竞争强度下均能安全运行。
-
兼容性设计
- 支持
hashCode()、wait()等基础方法,避免功能缺失。
- 支持
四、锁升级的典型问题
1. 偏向锁的争议
-
Java 15+ 默认禁用:现代应用多线程竞争复杂,偏向锁的撤销成本可能超过收益。
-
手动调优建议:
# 启用偏向锁(需权衡场景) -XX:+UseBiasedLocking # 设置批量重偏向阈值 -XX:BiasedLockingBulkRebiasThreshold=20
2. 重量级锁的性能陷阱
-
阻塞代价高:可通过减小锁粒度或使用无锁数据结构(如
ConcurrentHashMap)规避。 -
监控工具:
jstack:查看线程阻塞状态。JFR(Java Flight Recorder):分析锁竞争热点。
五、总结
锁升级是 JVM 在多线程编程中 权衡性能与安全 的经典设计:
- 触发场景:从单线程访问到高竞争逐步升级。
- 设计动机:通过渐进式优化,覆盖从极致性能到严格安全的全场景需求。
- 实际应用:理解锁升级机制可帮助开发者编写高效同步代码,避免性能反模式。