synchronize 实现原理
同步代码块是使用 monitorenter 和 monitorexit 指令实现的,同步方法(在这看不出来需要看 JVM 底层实现)依靠的是方法修饰符上的 ACC_SYNCHRONIZED 实现。
这两条字节码指令背后是同一套机制:Monitor(管程/监视器锁)。本文从字节码、对象头、锁升级三个层次把 synchronized 拆透,并说清它为什么从”重量级”变成了今天的样子。
字节码层:两种同步形态
同步代码块编译后在前后分别插入 monitorenter / monitorexit(异常路径还有一对隐式的 monitorexit,保证锁一定释放):
javap -c SyncDemo.class |
同步方法没有这两条指令,而是方法访问标志置为 ACC_SYNCHRONIZED:JVM 调用方法时检查该标志,先获取 monitor 再执行方法体。形态不同,本质相同——都是对某个对象的 monitor 的获取与释放:代码块锁括号里的对象,实例方法锁 this,静态方法锁 Class 对象。
Monitor 与对象头
HotSpot 中 monitor 由 ObjectMonitor 实现,核心字段:_owner(持有线程)、_EntryList(阻塞等待队列)、_WaitSet(wait() 等待队列)、_count 等。但每次加锁都分配 ObjectMonitor 太重,于是锁状态被压缩记录在对象头 Mark Word 里,通过最后几位标志位区分:
- 无锁
- 偏向锁(标记偏向哪个线程)
- 轻量级锁(指向栈中 Lock Record 的指针)
- 重量级锁(指向 ObjectMonitor 的指针)
锁升级:从无竞争到重竞争
JVM 的策略是”按需升级、不可降级”:
- 偏向锁:假设锁总被同一线程获取,第一次获取时在 Mark Word 记录线程 ID,之后该线程进入同步块只需一次 CAS 甚至无 CAS。(JDK15 起默认禁用偏向,因现代应用竞争模式变化且撤销成本高)
- 轻量级锁:出现第二个线程竞争时升级。线程在栈上建 Lock Record,用 CAS 把 Mark Word 替换为指向 Lock Record 的指针;获取失败则自旋重试若干次
- 重量级锁:自旋失败(竞争持续)则膨胀为 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 与升级路径,就能解释它几乎所有行为特征。











