深入了解 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 。

相关推荐
Fatbobman(东坡肘子)5 小时前
快速消失的硬盘容量 -- 肘子的 Swift 周报 #160
开发语言·人工智能·redis·ios·swiftui·iphone·swift
揽星河..5 小时前
【C语言】底层程序设计
c语言·开发语言
JavaPub-rodert5 小时前
从零搭建通用管理后台:ShiyuAdmin 的 Go、React 与 RBAC 实践
开发语言·react.js·golang
海的透彻6 小时前
dockerfile如何快速构建容器(java服务)
java·开发语言
code2cat6 小时前
Java工具篇之Gradle:用任务图组织构建与交付
java·开发语言·单元测试·gradle
北京中科新远科技7 小时前
高速交换机端口预算怎么做?下行、上联、拆分和冗余计算框架
开发语言·php
xiaoye-duck7 小时前
《C++ 项目:手写大模型接入 SDK》DeepSeek接口设计(下):流式响应实现和测试
开发语言·网络·c++·语言模型
后台模板学习7 小时前
大模型 Prompt 工程在电商订单系统中的踩坑记录
java·python·prompt
w_zero_one7 小时前
算法--构造
java·数据结构·算法
SL_staff7 小时前
制造业订单交付崩盘?用四层协同框架重建交付可信度(技术实践)
java·产品·全栈