如果你去面试 Java 并发,synchronized 的锁升级几乎是必考题。但很多人背完"偏向锁 → 轻量级锁 → 重量级锁"之后,心里还是犯嘀咕:为什么要分三种?什么时候用哪种?为什么竞争了就要升级?

这篇文章不讲大道理,用一个"会议室"的例子,把 synchronized 锁升级这件事彻底讲透。


先搞清楚一个问题:synchronized 为什么需要优化?

早期的 synchronized 很"粗暴"——不管你用不用得着,一律走重量级锁,底层依赖操作系统的 Monitor,涉及用户态和内核态的切换、线程的挂起和唤醒,开销非常大。

但实际开发中,绝大多数 synchronized 根本不存在竞争

  • 很多锁只被一个线程反复使用(比如单例模式)
  • 很多锁被多个线程交替使用,但时间错开,不会同时抢

如果这些场景都走重量级锁,就太浪费了。所以 JDK 1.6 引入了锁升级机制:根据竞争程度,自动选择最合适的锁实现


用"会议室"的例子理解三种锁

假设公司有一间会议室,三种锁就是三种管理方式。

偏向锁:专属工位(几乎零成本)

场景:这间会议室几乎天天被同一个人(比如老王)用,别人很少用。

做法

  • 第一次老王进来时,在门口贴个牌子写上"老王专属"(CAS 操作,把线程 ID 写进对象头的 Mark Word)
  • 之后老王每次进来,看一眼牌子是不是自己的名字,是就直接进去,什么都不用做
  • 如果别人来了,发现牌子是老王的名字,说明发生了竞争,锁升级

开销:只有第一次贴牌子时有一次 CAS,之后全是简单的比较操作,几乎零成本。

就像你的私人车位,每次回来直接停进去,不用刷卡不用登记。

轻量级锁:轮流使用,不撞车(CAS 自旋)

场景:会议室被多个人交替使用,但大家用的时间错开了,不会同时抢。

做法

  • 门口放一个登记本,谁要用就在本子上签个名
  • 你来的时候,看一眼登记本是不是空的
    • 空的 → 签上你的名字,进去用
    • 有人了 → 在门口等一小会儿(自旋),看看里面的人是不是快出来了
    • 等了一会儿还没出来 → 说明竞争比较激烈,升级成重量级锁

开销:用 CAS 操作修改登记本,比重量级锁轻得多,但比偏向锁重。

就像共享单车,你骑完锁上,下一个人来扫码骑走,大家轮流用,基本不撞车。

重量级锁:保安看守,排队叫号(操作系统介入)

场景:会议室很多人同时抢,天天打架。

做法

  • 请一个保安(操作系统内核的 Monitor)站在门口
  • 谁想用,先找保安登记
  • 里面有人?保安让你去旁边坐着等(线程挂起,进入阻塞状态)
  • 里面的人出来了,保安叫下一个
  • 坐着等的人什么都不干,不消耗 CPU

开销:涉及用户态和内核态的切换、线程的挂起和唤醒,成本非常高。

就像医院挂号看病,你得排队、叫号、等医生,整个过程很重。


锁升级的完整流程

锁的升级路径是:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,且只能升级不能降级。

第一次加锁,只有一个线程用
  → 偏向锁(贴个名字就行)
      │
      │ 第二个线程来抢了
      ▼
  轻量级锁(CAS + 自旋等待)
      │
      │ 自旋多次还抢不到,竞争太激烈
      ▼
  重量级锁(Monitor,操作系统介入,线程挂起)

这里有一个容易搞错的点:偏向锁遇到竞争时,不是直接跳到重量级锁,而是先升级为轻量级锁。只有轻量级锁在自旋多次后仍然抢不到,才会进一步升级为重量级锁。一步一步来,不是一步到位。


为什么"一旦竞争就升级"?

关键在于成本对比。

锁类型适用场景竞争时的代价
偏向锁只有一个线程反复用撤销偏向要遍历所有线程的栈,开销大
轻量级锁多线程交替用,不撞车自旋空转浪费 CPU,越竞争越浪费
重量级锁多线程激烈竞争虽然切换开销大,但线程挂起不耗 CPU

当竞争发生时:

  • 偏向锁发现别人也来抢了,"专属工位"模式失效,撤销偏向,升级为轻量级锁
  • 轻量级锁发现自旋等了很久对方还没释放,说明竞争激烈,自旋只会白白浪费 CPU,升级为重量级锁,让线程老老实实去睡觉等待

升级的本质是:当"轻量方案"已经扛不住时,果断换成"重量方案",避免更大的浪费。


三种锁的底层细节

偏向锁的底层

对象头的 Mark Word 中存储了持有锁的线程 ID。第一次加锁时,通过 CAS 将线程 ID 写入 Mark Word。之后该线程再次进入同步块时,只需比较 Mark Word 中的线程 ID 是否等于当前线程 ID,相等则直接进入,无需任何额外操作。

当另一个线程尝试获取偏向锁时,发现 Mark Word 中的线程 ID 不是自己的,就会触发偏向撤销。撤销偏向锁需要等到安全点(Safe Point),此时持有锁的线程已经不再执行同步块代码,然后才能将锁升级为轻量级锁。

轻量级锁的底层

每个线程在自己的栈帧中创建一个锁记录(Lock Record),然后通过 CAS 尝试将对象头 Mark Word 中的指针指向自己的锁记录。如果 CAS 成功,说明加锁成功;如果失败,说明有其他线程也在竞争,此时线程会进行自旋(循环重试),等待对方释放锁。

自旋不是无限的。如果自旋超过一定次数仍然没有拿到锁,说明竞争比较激烈,继续自旋只会浪费 CPU,此时锁升级为重量级锁。

重量级锁的底层

重量级锁依赖操作系统的 Mutex Lock(互斥锁)实现。当锁升级为重量级锁后,对象头的 Mark Word 指向一个 Monitor 对象。Monitor 内部维护了三个队列:

  • EntryList:存放被阻塞、等待获取锁的线程
  • WaitSet:存放调用了 wait() 方法后等待通知的线程
  • Owner:当前持有锁的线程

未获取到锁的线程会被挂起(park),进入阻塞状态,不再消耗 CPU。持有锁的线程释放锁后,会唤醒 EntryList 中的线程。


一张表总结

表格

偏向锁轻量级锁重量级锁
适用场景只有一个线程反复加锁多线程交替加锁,无竞争多线程激烈竞争
加锁方式比较线程 IDCAS + 自旋Monitor,线程挂起
是否涉及操作系统
性能最高较高较低
对象头 Mark Word存储线程 ID指向栈中锁记录指向 Monitor

一句话总结

偏向锁 = 私人专属(零开销),轻量级锁 = 轮流用不撞车(CAS 自旋),重量级锁 = 保安看守排队(操作系统介入)。锁的升级就像公司管理方式的变化:人少时靠自觉,人多了靠登记,打架了才请保安。

synchronized 的锁升级机制,本质上是 JVM 在"性能"和"安全"之间做出的智能权衡——绝大多数时候锁是没有竞争的,所以用最低成本的方式处理;只有真正出现竞争时,才付出更高的代价来保证线程安全。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐