深入了解 Java `synchronized`:从对象头到锁升级、线程竞争感知

在 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 会禁用偏向锁------两者在物理上共用同一块存储空间。


五、偏向锁如何升级为轻量级锁?

这是很多人误解的环节。升级并非直接跳转,而是分步完成

  1. 线程 T2 尝试获取已被 T1 偏向的对象锁
  2. JVM 发现 Mark Word 中的线程 ID ≠ T2。
  3. 检查 T1 状态:
    • 若 T1 已退出同步块 → 可能 重偏向 给 T2(仍为偏向锁)。
    • 若 T1 仍在运行 → 触发 偏向撤销(Bias Revocation)(需 Safepoint)。
  4. 撤销后,对象变为普通无锁状态(biased_lock=0, lock=01)。
  5. 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

相关推荐
侠客行03174 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪4 小时前
深入浅出LangChain4J
java·langchain·llm
灰子学技术6 小时前
go response.Body.close()导致连接异常处理
开发语言·后端·golang
老毛肚6 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎6 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
二十雨辰6 小时前
[python]-AI大模型
开发语言·人工智能·python
Yvonne爱编码6 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚6 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂7 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
pas1367 小时前
41-parse的实现原理&有限状态机
开发语言·前端·javascript