synchronized原理与锁升级#
一句话答案#
synchronized 底层靠 Monitor 实现,JDK6 引入锁升级:无锁→偏向锁(单线程)→轻量级锁(CAS)→重量级锁(阻塞);偏向锁 JDK 15 起默认禁用、JDK 18 移除,现代 JDK 是 无锁→轻量级锁→重量级锁。
核心要点
synchronized 的底层实现(Monitor 对象监视器):
每个 Java 对象都可以关联一个 Monitor(监视器,HotSpot 里是 C++ 的 ObjectMonitor)。Monitor 不在对象头里,而是竞争升级为重量级锁时才创建,对象头(Mark Word)里存指向它的指针。
代码块锁:
synchronized(obj) { ... }
↓ 编译为字节码:
monitorenter ← 获取 obj 关联的 Monitor,计数+1
...
monitorexit ← 释放 Monitor,计数-1,计数=0时唤醒等待线程
方法锁:
synchronized void method() { ... }
↓ 方法访问标志中设置 ACC_SYNCHRONIZED,JVM 执行时自动 monitorenter/monitorexitplaintext锁升级(JDK 6 引入,解决 synchronized 重量级锁性能问题):
锁状态存储在对象头的 Mark Word 中,随竞争程度单向升级(不可降级;偏向锁 JDK 15 起默认禁用(JEP 374)、JDK 18 移除代码;JDK 23 起轻量级锁默认改为新的 LM_LIGHTWEIGHT 实现,用线程私有的 lock stack 代替栈上 Lock Record,下面讲的是经典 LM_LEGACY 流程):
无锁 → 偏向锁 → 轻量级锁(自旋锁)→ 重量级锁
无锁
↓ 第一个线程访问
偏向锁(Biased Locking)
- Mark Word 记录持有该锁的线程 ID
- 同一线程再次获取:直接检查 Mark Word 中的线程 ID,无需 CAS,极快
- 有第二个线程竞争时升级 ↓
轻量级锁(Thin Lock)
- 线程在自己的栈帧中创建 Lock Record(锁记录)
- 通过 CAS 将 Lock Record 地址写入 Mark Word
- CAS 成功 = 获锁;CAS 失败 = 有竞争 → 升级 ↓
- (常见说法是「轻量级锁自旋 10 次再升级」;HotSpot 实际是 CAS 失败即膨胀,自适应自旋发生在膨胀后的 ObjectMonitor 进入阻塞之前)
重量级锁(Fat Lock)
- Mark Word 指向 ObjectMonitor 对象(C++ 实现)
- 获锁失败的线程进入 EntryList(阻塞等待)
- 持锁线程 wait() 后进入 WaitSet(等待被 notify 唤醒)
- OS 级别的互斥量(Mutex),涉及用户态/内核态切换,开销大plaintext为什么阻塞前先自旋: 如果持锁时间很短(如纳秒级),与其让线程阻塞(涉及上下文切换,微秒级开销),不如让它 CAS 自旋几次等待,避免不必要的内核调用。
面试回答(2分钟版)
synchronized 底层通过 Monitor 对象监视器实现,代码块编译为 monitorenter 和 monitorexit 字节码,方法则在访问标志中设置 ACC_SYNCHRONIZED。JDK6 之前 synchronized 直接使用重量级锁,涉及用户态和内核态切换开销很大。JDK6 引入了锁升级机制,锁状态记录在对象头的 Mark Word 中,分为四个级别:无锁状态下第一个线程进入时升级为偏向锁,Mark Word 记录线程 ID,同一线程再次获取时直接比对 ID 免去 CAS,极快;当第二个线程来竞争时升级为轻量级锁,线程在栈帧中创建 Lock Record 并 CAS 将其地址写入 Mark Word;CAS 失败说明有竞争,膨胀为重量级锁,Mark Word 指向 ObjectMonitor,线程先自适应自旋几次,仍拿不到就进入 EntryList 阻塞等待。另外偏向锁在 JDK 15 已默认禁用、JDK 18 移除,所以现代 JDK 实际是无锁、轻量级锁、重量级锁三级。整个过程是单向升级不可降级。自旋的意义在于持锁时间很短时避免线程阻塞带来的上下文切换开销,比直接阻塞更高效。
追问与易错
追问方向:
- “锁升级后能降级吗?”→ 对象层面一般不降级;但空闲的 ObjectMonitor 会被 JVM 回收(monitor deflation,早期在安全点做,JDK 15 起由后台线程异步做),对象头回到无锁状态
- “JDK15 为什么默认关闭偏向锁?”→ 偏向撤销代价大,现代应用竞争多
- “synchronized 加在方法和代码块上的区别?”→ 方法用 ACC_SYNCHRONIZED 标志,代码块用 monitorenter/exit
易错点:
- ❌ “synchronized 性能很差”——JDK6 优化后轻量级锁开销很小
- ❌ 不理解”锁的是对象不是代码”——不同对象实例互不影响