synchronized 相关思考
前言
最近又重新过了一遍 synchronized,发现自己之前对它的理解其实偏“结论导向”。
比如知道它能保证原子性、可见性、有序性,也知道它背后有锁升级、对象头、Monitor 这些东西,但一旦继续追问“为什么会升级”“对象头里到底放了什么”“自旋和阻塞的边界在哪”,脑子里就容易变成一锅粥。
所以这篇笔记不打算写成教材式定义罗列,而是按几个我自己最想搞明白的问题,重新顺一遍。
先把问题列出来:
synchronized到底是怎么和对象头扯上关系的?- JVM 为什么不一上来就用重量级锁,而是要搞偏向锁、轻量级锁这一套?
- 自旋锁和线程阻塞到底差在哪?
wait()/notify()为什么一定要在同步块里用?synchronized的可重入、静态锁、锁消除这些点,底层分别在干什么?
正文
1. synchronized 到底锁的是什么?
先说结论:synchronized 最终锁住的不是“代码”,而是某个对象关联的监视器,也就是 Monitor。
这个事情之所以经常让人绕进去,是因为平时写代码时看到的是:
synchronized (obj) {
// critical section
}
表面上像是“锁代码块”,但 JVM 真正在竞争的是 obj 对应的锁状态,以及必要时膨胀出来的 ObjectMonitor。
这里最关键的入口是对象头里的 Mark Word。
Mark Word 这块空间在不同状态下会存不同东西。平时它可能存对象哈希、分代年龄;一旦进入同步场景,它又会切成锁相关的数据结构。
大致可以这样记:
| 锁状态 | Mark Word 里主要存什么 |
|---|---|
| 无锁 | hash、GC 年龄、锁标记位 |
| 偏向锁 | 偏向线程 ID、时间戳、年龄等 |
| 轻量级锁 | 指向线程栈中Lock Record 的指针 |
| 重量级锁 | 指向Monitor 的指针 |
所以 synchronized 的底层切入点,本质上就是对象头状态的变化。
2. JVM 为什么要搞锁升级?
一开始我也觉得这套设计挺绕的,直接一个互斥锁不就完了。
但问题在于,线程同步的场景差异太大了。
有的锁根本没有竞争,只是单线程反复进入同步块;有的锁会偶尔被多个线程碰到,但竞争时间很短;还有的锁竞争激烈,线程必须老老实实挂起等待。
如果所有场景都直接走重量级锁,那开销会很大,因为线程阻塞和唤醒最终会涉及内核态切换。
所以 JVM 才设计了一个逐步升级的路线:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
2.1 偏向锁解决什么问题
偏向锁针对的是“几乎没有竞争”的场景。
如果一个同步块总是被同一个线程访问,那每次都去 CAS 抢锁其实挺浪费的。于是 JVM 干脆把线程 ID 记到对象头里,后面只要还是这个线程来,就默认它继续持有这个偏向关系,不用重复做同步竞争动作。
这个思路很适合单线程反复进入同步块的情况。
不过偏向锁不是没有代价。它一旦遇到第二个线程参与竞争,就要撤销偏向,而撤销这个动作是有成本的,严重时还会牵扯到安全点。
所以偏向锁的收益建立在“真的没什么竞争”这个前提上。
2.2 轻量级锁解决什么问题
当偏向锁不成立,或者本来就存在多个线程交替进入同步块时,就会进入轻量级锁阶段。
这时 JVM 不会立刻阻塞线程,而是先用 CAS 去尝试把对象头里的 Mark Word 替换成指向当前线程栈帧里 Lock Record 的指针。
如果 CAS 成功,说明当前线程拿到了锁。
如果失败,说明存在竞争,但 JVM 还是会先比较克制,通常不会马上把线程挂起,而是先尝试自旋。
2.3 重量级锁什么时候出现
当自旋也拿不到锁,或者竞争已经明显激烈时,锁就会膨胀成重量级锁。
这时候对象头里会指向 Monitor,线程竞争也会进入真正的阻塞/唤醒模型。
到这里就能看出来,锁升级的核心目标不是“复杂化”,而是在不同竞争强度下尽量选择成本更合适的同步方式。
3. 自旋锁和线程阻塞到底差在哪?
这个点很容易只停在一句话上:一个忙等,一个挂起。
但真正值得记住的是它们适合的场景完全不同。
3.1 自旋的核心
自旋的意思是:线程暂时不睡,自己循环再试几次。
伪代码可以理解成这样:
while (true) {
if (CAS_try_lock()) {
break;
}
}
这样做的好处是避免线程在很短时间内频繁阻塞和唤醒,因为上下文切换本身就不便宜。
如果锁持有时间很短,自旋可能很快就成功,整体成本比挂起再唤醒更低。
3.2 自旋的问题
自旋的问题也很明显:它会占着 CPU。
如果锁迟迟不释放,线程就会一直空转。竞争一旦激烈,或者临界区本身就比较长,自旋反而是在浪费算力。
所以 JVM 后来用了自适应自旋,不是死板地自旋固定次数,而是会参考历史上这个锁的自旋成功率、持锁线程状态等信息,动态决定要不要继续转。
这个优化思路很现实,不是“自旋永远更快”,而是“短竞争下尽量少阻塞,长竞争下早点认输”。
4. Monitor 到底在干什么?
当锁膨胀到重量级后,Monitor 就成了主角。
可以把它理解成 JVM 提供的一套管程模型,里面至少有几个关键角色:
_owner:当前持有锁的线程_EntryList:等待进入同步块的线程队列_WaitSet:调用了wait()后暂时挂起的线程队列
线程抢锁时,会先尝试把 _owner 设置成自己。
如果拿不到锁,就不能继续执行临界区逻辑,只能进入等待队列。
这里也能顺手把 wait() / notify() 的语义理一下。
4.1 为什么 wait() 必须在同步块里调用
因为 wait() 不是一个普通的“睡眠”动作,它会直接操作当前对象的 Monitor 状态。
调用 wait() 后,线程会释放当前持有的锁,并进入 _WaitSet。
这意味着它不是单纯让线程暂停一下,而是在修改这把锁内部的等待结构。
既然要改 Monitor 的内部状态,那前提当然得先持有这把锁。不然谁都能随便改等待队列,整个同步语义就乱了。
4.2 notify() 做了什么
notify() 会从 _WaitSet 里唤醒一个等待线程,把它转移回可参与竞争的位置。
这里要注意,notify() 不是“立刻让对方继续执行”,它只是给对方一个重新竞争锁的机会。
真正能不能继续往下跑,还得看它重新拿锁是否成功。
5. synchronized 为什么是可重入的?
所谓可重入,就是同一个线程拿到一把锁之后,可以再次进入这把锁保护的同步区域,而不会把自己卡死。
这个特性很关键,不然同步方法之间互相调用会非常容易出问题。
底层可以理解成:锁内部会记录“当前持有者是谁”和“这个持有者已经重入了多少次”。
如果当前线程已经是持有者,那么再次进入时不会重新发起一轮完整竞争,而是把重入计数加一;退出同步块时再递减,直到计数归零才真正释放锁。
所以它不是“重复拿了很多把锁”,而是“同一把锁被同一线程多次进入,并通过计数维护层级关系”。
6. 静态同步方法和实例同步方法,锁的是同一个东西吗?
不是。
这个问题面试里很常见,但平时如果不刻意想,很容易混掉。
- 实例同步方法锁的是当前对象,也就是
this - 静态同步方法锁的是这个类对应的
Class对象
所以这两类锁默认不会互相竞争,因为它们压根不是同一个监视器。
这也是为什么同一个类里,线程 A 调用实例同步方法,线程 B 调用静态同步方法,不一定会互相阻塞。
7. synchronized 和 ReentrantLock 怎么区分理解?
这个问题其实不该只背“前者是关键字,后者是 API”。
更实用的理解方式是:
synchronized是 JVM 原生支持的同步机制,语义简单,自动释放锁,锁升级这些优化也由 JVM 兜底ReentrantLock是 JUC 提供的显式锁,功能更灵活,比如可中断、可轮询、可指定公平策略、可绑定多个条件队列
如果只是普通同步控制,synchronized 已经够用,而且代码更直。
如果业务需要更细的锁控制能力,比如可中断获取锁、公平锁、多个条件变量,那 ReentrantLock 会更合适。
8. 锁消除和锁粗化,算是 JVM 的哪些“小心思”?
这两个点平时不太会主动想到,但它们很能体现 JVM 的优化思路。
8.1 锁消除
如果 JVM 通过逃逸分析发现某个锁对象根本不会逃逸出当前线程,那这个同步其实没有竞争可能。
既然不可能有竞争,那这个锁就没有存在意义,直接消掉就行。
8.2 锁粗化
如果代码里有很多连续的加锁/解锁操作,JVM 可能会把它们合并成一个更大范围的锁。
虽然看起来临界区变大了,但如果能减少频繁加锁解锁的成本,整体反而更划算。
这两个优化都说明了一件事:JVM 不是机械地执行 synchronized,而是在尽量判断“这把锁到底要不要这么认真地锁”。
总结
这次重新梳下来,我觉得 synchronized 最容易记乱的地方,不是概念多,而是每个概念都只记了一半。
比如知道对象头,但没把它和 Mark Word、锁状态切换连起来;知道轻量级锁会自旋,但没想清楚它和重量级阻塞的成本边界;知道 wait() / notify() 必须在同步块里用,却没继续往下追它到底在改谁的状态。
把这些点串起来之后,整个模型会清楚很多:
- 对象头里的
Mark Word是锁状态入口 - 锁升级是 JVM 在不同竞争强度下做的成本权衡
- 轻量级锁更依赖 CAS 和自旋
- 重量级锁最终落到
Monitor的阻塞/唤醒机制 - 可重入、本地优化、静态锁对象这些点,都是这套机制上的自然延伸
如果后面再被问 synchronized,我觉得不要一上来就背定义,顺着“对象头 -> 锁升级 -> 自旋 -> Monitor -> 可重入”这条链路讲,基本就不容易散了。