同步代码块是使用 monitorenter 和 monitorexit 指令实现的,同步方法(在这看不出来需要看 JVM 底层实现)依靠的是方法修饰符上的 ACC_SYNCHRONIZED 实现。

这两条字节码指令背后是同一套机制:Monitor(管程/监视器锁)。本文从字节码、对象头、锁升级三个层次把 synchronized 拆透,并说清它为什么从”重量级”变成了今天的样子。

字节码层:两种同步形态

同步代码块编译后在前后分别插入 monitorenter / monitorexit(异常路径还有一对隐式的 monitorexit,保证锁一定释放):

javap -c SyncDemo.class
# 8: monitorenter
# 9: aload_0
# 10: invokevirtual ...
# 13: monitorexit

同步方法没有这两条指令,而是方法访问标志置为 ACC_SYNCHRONIZED:JVM 调用方法时检查该标志,先获取 monitor 再执行方法体。形态不同,本质相同——都是对某个对象的 monitor 的获取与释放:代码块锁括号里的对象,实例方法锁 this,静态方法锁 Class 对象。

Monitor 与对象头

HotSpot 中 monitor 由 ObjectMonitor 实现,核心字段:_owner(持有线程)、_EntryList(阻塞等待队列)、_WaitSet(wait() 等待队列)、_count 等。但每次加锁都分配 ObjectMonitor 太重,于是锁状态被压缩记录在对象头 Mark Word 里,通过最后几位标志位区分:

  • 无锁
  • 偏向锁(标记偏向哪个线程)
  • 轻量级锁(指向栈中 Lock Record 的指针)
  • 重量级锁(指向 ObjectMonitor 的指针)

锁升级:从无竞争到重竞争

JVM 的策略是”按需升级、不可降级”:

  1. 偏向锁:假设锁总被同一线程获取,第一次获取时在 Mark Word 记录线程 ID,之后该线程进入同步块只需一次 CAS 甚至无 CAS。(JDK15 起默认禁用偏向,因现代应用竞争模式变化且撤销成本高)
  2. 轻量级锁:出现第二个线程竞争时升级。线程在栈上建 Lock Record,用 CAS 把 Mark Word 替换为指向 Lock Record 的指针;获取失败则自旋重试若干次
  3. 重量级锁:自旋失败(竞争持续)则膨胀为 ObjectMonitor,未获取到的线程进入内核态阻塞(park),释放时唤醒

升级的意义在于:无竞争时零内核调用(偏向),低竞争时用户态自旋(轻量级),高竞争时才付出阻塞/唤醒的系统调用成本(重量级)。锁不是非黑即白,而是一条按竞争强度自适应的光谱。

wait/notify 的去向

调用 obj.wait() 时,持有锁的线程把自身从 _owner 移入 _WaitSet 并释放锁;notify() 把 _WaitSet 中线程移入 _EntryList 参与下一轮竞争。这解释了为什么 wait 必须在同步块里——没有持有 monitor 就谈不上”释放 monitor 去等待”。

与 ReentrantLock 的取舍

维度 synchronized ReentrantLock
实现 JVM 指令 + monitor JDK 层 AQS
释放 自动(含异常路径) 必须 finally unlock
中断/超时响应 不支持 lockInterruptibly / tryLock
公平锁 非公平 可选公平
条件队列 单个 wait/notify 多个 Condition

经验法则:能用 synchronized 就用它(JVM 持续优化、不会忘释放);需要超时、中断、多条件、公平性时才上 ReentrantLock。

使用注意

  • 锁对象要选对:锁字符串常量/ intern 串会造成意外全局锁;锁对象被回收或替换会导致锁失效
  • 同步块尽量小:把 IO、远程调用移出同步块,否则重量级锁下的阻塞会放大竞争
  • 别在锁内调用外部回调:回调可能重入或长时间执行,破坏锁的预期范围

synchronized 从”笨重的关键字”进化为带锁升级的自适应同步原语,理解 Mark Word 与升级路径,就能解释它几乎所有行为特征。