synchronized 是 Java 并发编程中最常用的关键字。它的底层实现并不是一成不变的,而是会根据竞争情况逐步升级锁的状态,以兼顾性能和线程安全。本文将按照 锁升级的流程,逐步解析每个阶段的原理,并插入相关的知识点。
一、无锁阶段:对象头中的 Mark Word
在 HotSpot JVM 中,每个对象都有一个对象头(Object Header),其中的 Mark Word 用来存储运行时信息:
哈希码(identity hashcode)
GC 分代年龄
锁状态标志位
此时对象是“自由”的,没有任何线程持有锁。
二、偏向锁:优化单线程场景
当线程 A 第一次进入 synchronized(obj):
JVM 会在对象头的 Mark Word 中写入 线程 A 的 ID,并标记为偏向锁。
后续同一线程再次进入同步块时,不需要 CAS 操作,直接认为自己持有锁。
知识点插入:
偏向锁的设计是为了优化“单线程反复进入同步块”的场景。
但偏向锁和哈希码互斥:如果调用
hashCode(),偏向锁会撤销。
偏向锁撤销的场景:
1. 调用
hashCode()
Mark Word 原本存放线程 ID,此时无法同时存储哈希码。
JVM 会撤销偏向锁,升级为轻量级锁。
哈希码会被 重新计算并搬到 Lock Record 中保存。
2. 新线程竞争
如果线程 B 尝试进入同步块,偏向锁会撤销。
对象头的 Mark Word 改为指向线程栈中的 Lock Record。
三、轻量级锁:避免阻塞的尝试
轻量级锁的核心是 CAS + Lock Record:
线程尝试通过 CAS 修改对象头,指向自己的 Lock Record。
如果成功,进入同步块;如果失败,会进入 自旋,继续尝试。
知识点插入:
轻量级锁的意义在于 避免阻塞,在低竞争场景下提升性能。
新线程的去向:
CAS 成功 → 直接进入同步块。
CAS 失败但仍有机会 → 自旋等待。
CAS 多次失败 → 升级为重量级锁。
四、重量级锁:Monitor 出场
当竞争激烈,自旋失败时,锁会膨胀为重量级锁:
JVM 创建一个 Monitor,对象头的 Mark Word 改为指向这个 Monitor。
Monitor 内部结构:
Owner:当前持有锁的线程。
EntryList:竞争失败的线程队列。
WaitSet:调用
wait()的线程集合。Recursion Count:支持锁的可重入。
知识点插入:
monitorenter和monitorexit字节码指令就是对 Monitor 的操作。EntryList 和 WaitSet 的区别:
EntryList:竞争失败的线程,被动等待锁释放。
WaitSet:主动调用
wait()的线程,等待notify()唤醒。
wait 和 notify 的使用场景
1. 生产者-消费者模型
生产者线程:不断往共享队列里放数据。
消费者线程:不断从共享队列里取数据。
问题:如果队列满了,生产者不能再放;如果队列空了,消费者不能取。
解决办法:
消费者在队列空时调用
wait(),进入 WaitSet,释放锁,等待数据。生产者放入数据后调用
notify()或notifyAll(),唤醒消费者。👉 这是最典型的场景。
2. 线程间条件等待
某个线程需要等待一个条件成立才能继续执行。
比如:一个线程要等另一个线程完成初始化。
这时可以用
wait()挂起自己,等条件满足后由其他线程调用notify()唤醒。3. 替代轮询
如果不用
wait(),线程可能会不断轮询检查条件(比如while(!ready){}),浪费 CPU。
wait()可以让线程进入阻塞状态,直到被唤醒,避免忙等。
五、锁升级流程总结
完整的锁升级路径如下:
代码
无锁 → 偏向锁(线程ID)
→ 调用hashCode撤销偏向锁(哈希码搬到Lock Record)
→ 轻量级锁(CAS + 自旋)
→ 多线程竞争激烈 → 重量级锁(Monitor)
核心思想:
偏向锁和轻量级锁是优化路径,避免阻塞,提高性能。
重量级锁是兜底方案,保证线程安全。
哈希码不会丢失,只是根据锁状态被转移到不同位置(Lock Record 或 Monitor)。
六、结语
通过锁升级的流程,我们可以看到 JVM 的设计哲学:
空间有限 → 位复用(Mark Word 在不同状态下存储不同信息)。
性能优先 → 常见场景快,少数场景复杂。
功能兼容 → 哈希码和锁状态都能保留。
理解这些机制,不仅能帮助我们更好地使用 synchronized,也能在实际开发中更合理地选择并发工具(如 ReentrantLock、StampedLock 等)。
理解 Java 中的 synchronized 原理
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法