Administrator
发布于 2026-06-12 / 0 阅读
0
0

synchronized相关思考

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. synchronizedReentrantLock 怎么区分理解?

这个问题其实不该只背“前者是关键字,后者是 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 -> 可重入”这条链路讲,基本就不容易散了。


评论