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

相关推荐
silvamux_9 分钟前
curl 测试 OpenAI 兼容 API 的完整流程
java·人工智能·缓存·ai·oneapi
省钱兄--zs39 分钟前
深圳24小时自助健身房软硬件解决方案实战指南与开发流程详解
java·数据仓库·spring boot·小程序·架构
顽疲1 小时前
Java vue 养老系统源码详解:月账单 care_month_bill 与 Quartz 扣费 Job 实践
java·开发语言·vue.js·vue养老·springboot养老·java养老
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 安装开发工具与环境准备
java·开发语言·交友
用户21296835261681 小时前
开源了一个 Java 多智能体 LLM 框架 Baton:一个依赖,把 Agent 循环、工具调用、记忆、编排全配齐
java·llm
helmsail1 小时前
05 管理活动与选品
java
曹牧2 小时前
C#:输入的字符串格式不正确
c#
天远数科2 小时前
Java数据工程:利用天远人车核验加强版优化智慧高速ETC发行合规体验
java·人工智能
.Bryce.2 小时前
在智能体中使用A2A工具:让大模型自己决定“该找谁帮忙“
android·java·服务器·redis·后端·agent·a2a
xiaoqiMikko2 小时前
fastjson 1.2.84 的补丁关掉了一个缓存:addAccept 包前缀白名单下,反序列化慢了 13~28 倍
java·后端·性能优化