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:支持锁的可重入。

知识点插入:

  • monitorentermonitorexit 字节码指令就是对 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,也能在实际开发中更合理地选择并发工具(如 ReentrantLockStampedLock 等)。