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

相关推荐
Coder_Boy_5 分钟前
基于SpringAI的在线考试系统-企业级软件研发工程应用规范实现细节
大数据·开发语言·人工智能·spring boot
lly2024069 分钟前
SQL SELECT 语句详解
开发语言
5***b979 分钟前
Spring Boot--@PathVariable、@RequestParam、@RequestBody
java·spring boot·后端
AIGCExplore25 分钟前
Jenkins 全局配置及工具验证教程
java·servlet·jenkins
qq_3181215930 分钟前
Java大厂面试故事:Spring Boot、微服务与AI场景深度解析
java·spring boot·redis·微服务·ai·kafka·spring security
superman超哥31 分钟前
Rust 异步时间管理核心:Tokio 定时器实现机制深度剖析
开发语言·rust·编程语言·rust异步时间管理核心·tokio定时器实现机制·tokio定时器
朔北之忘 Clancy32 分钟前
2025 年 9 月青少年软编等考 C 语言一级真题解析
c语言·开发语言·c++·学习·数学·青少年编程·题解
玛丽莲茼蒿35 分钟前
javaSE 集合框架(五)——java 8新品Stream类
java·开发语言
wjs202439 分钟前
SQLite Glob 子句详解
开发语言
程序员小假42 分钟前
设计一个支持万人同时抢购商品的秒杀系统?
java·后端