在 Java 并发编程中,synchronized 是最基础、最常用的同步机制。它看似简单,背后却隐藏着 JVM 多年演进的精妙设计------从对象内存布局到锁状态动态升级,从线程竞争感知到零开销优化。本文将带你穿透语法糖,深入 HotSpot 虚拟机内部,完整还原 synchronized 的底层实现原理。
一、synchronized 的本质:Monitor 与对象头
synchronized 的核心是 监视器锁(Monitor Lock) 。每个 Java 对象在堆内存中都包含一个 对象头(Object Header) ,而锁信息就存储在对象头的 Mark Word 字段中。
在 64 位 JVM(开启指针压缩)中,Mark Word 占 8 字节,其内容会随对象状态动态变化:
| 锁状态 | Mark Word 内容(关键字段) |
|---|---|
| 无锁 | hashcode + GC age + biased_lock=0, lock=01 |
| 偏向锁 | thread ID + epoch + age + biased_lock=1, lock=01 |
| 轻量级锁 | 指向 Lock Record 的指针 + lock=00 |
| 重量级锁 | 指向 ObjectMonitor 的指针 + lock=10 |
✅ 关键洞察 :
synchronized不依赖额外数据结构,而是复用对象自身的 Mark Word 来实现锁状态管理,极大节省内存开销。
二、锁的进化:从偏向锁到重量级锁
JVM 采用 锁升级(Lock Inflation) 策略,在保证线程安全的前提下,尽可能降低同步成本。整个路径如下:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
1. 偏向锁(Biased Locking):单线程的零成本优化
- 适用场景:只有一个线程反复进入同步块。
- 实现原理 :
- 首次加锁时,通过 CAS 将当前线程 ID 写入 Mark Word。
- 后续同一线程再次进入,只需比对线程 ID,无需任何原子操作。
- 标志位 :
biased_lock=1表示对象处于可偏向状态。
⚠️ 注意:一旦调用
obj.hashCode()或System.identityHashCode(obj),JVM 会将 hash code 存入 Mark Word,导致 永久禁用偏向锁(因空间冲突)。
2. 轻量级锁(Lightweight Locking):低竞争下的自旋优化
- 触发条件:多个线程交替访问(无真正并发)。
- 实现原理 :
- 在当前线程栈中创建 Lock Record。
- CAS 尝试将 Mark Word 替换为指向 Lock Record 的指针。
- 成功则获得锁;失败则自旋重试。
- 锁状态 :
lock=00
3. 重量级锁(Heavyweight Locking):高竞争下的 OS 级阻塞
- 触发条件:自旋失败或竞争激烈。
- 实现原理 :
- 在堆中分配 C++ 实现的 ObjectMonitor。
- Mark Word 指向该 Monitor(
lock=10)。 - 竞争线程被挂起(
park()),由操作系统调度。
三、JVM 如何"感知"线程竞争?
JVM 并不主动监控竞争,而是通过 操作反馈机制 动态感知:
| 阶段 | 感知方式 | 触发动作 |
|---|---|---|
| 偏向锁 | 当前线程 ID ≠ Mark Word 中的线程 ID | 尝试撤销或重偏向 |
| 轻量级锁 | CAS 替换 Mark Word 失败 | 自旋 → 锁膨胀 |
| 重量级锁 | Monitor 的 _owner 非空 |
线程阻塞(park) |
🔑 核心思想 :
"先乐观尝试,失败再升级" ------ 这是一种低开销、反馈驱动的并发控制模型。
此外,JVM 还具备 自适应自旋 能力:根据历史成功/失败记录,动态调整自旋次数,避免 CPU 浪费。
四、biased_lock 标志位的深层作用
biased_lock 是 Mark Word 中的 1 位标志,但它至关重要:
biased_lock=1:对象支持偏向锁,Mark Word 前 54 位解释为 线程 ID。biased_lock=0:对象不可偏向,Mark Word 前 31 位解释为 hash code。
💡 为什么需要这个标志?
因为 Mark Word 是复用字段!没有
biased_lock,JVM 无法区分一段二进制数据到底是"线程 ID"还是"哈希码"。
这也解释了为何计算 identity hash code 会禁用偏向锁------两者在物理上共用同一块存储空间。
五、偏向锁如何升级为轻量级锁?
这是很多人误解的环节。升级并非直接跳转,而是分步完成:
- 线程 T2 尝试获取已被 T1 偏向的对象锁。
- JVM 发现 Mark Word 中的线程 ID ≠ T2。
- 检查 T1 状态:
- 若 T1 已退出同步块 → 可能 重偏向 给 T2(仍为偏向锁)。
- 若 T1 仍在运行 → 触发 偏向撤销(Bias Revocation)(需 Safepoint)。
- 撤销后,对象变为普通无锁状态(
biased_lock=0, lock=01)。 - T2 按照 轻量级锁流程 加锁(CAS + Lock Record)。
✅ 结论 :
线程 ID 比较是"发现问题"的起点,但"解决问题"靠的是锁撤销和轻量级锁机制。
六、实践建议
- 不要盲目禁用偏向锁:在单线程或低竞争场景(如 Spring Bean 访问),它能显著提升性能。
- 避免在同步块内调用
hashCode():这会提前终结偏向锁优化。 - 高并发服务可考虑关闭偏向锁 :如 Kafka、Netty 使用
-XX:-UseBiasedLocking,减少撤销开销。 - 优先缩小同步范围 :
synchronized虽经优化,但仍应只保护必要临界区。
结语
synchronized 从 Java 1.0 的"重量级原罪",到 JDK 1.6+ 的"高效同步原语",其演进史就是 JVM 并发优化的缩影。理解它,不仅是为了写出线程安全的代码,更是为了掌握 如何在正确性与性能之间取得平衡。
而这一切,都始于对象头中那 64 位不断变幻的 Mark Word。
📚 延伸工具推荐:
- 使用 JOL(Java Object Layout) 查看对象头实时状态
- 通过 -XX:+PrintBiasedLockingStatistics 观察偏向锁统计
- 结合 JITWatch 分析 synchronized 的 JIT 编译优化
问题
1. 会不会由偏向锁直接转换成重量级锁?
2. 锁如果不能退化,那么升级为重量级锁后岂不是性能很低?
3. 为什么偏向锁要撤销后才能升级轻量级锁?
本文最终由AI生成,希望这篇文章能帮您更深入了解 synchronized 。