← 返回题目列表

synchronized 的底层原理是什么?锁升级是怎么回事?

高频 困难 第 19 / 31 题 更新于 2026/07/25
synchronized锁升级Monitor对象头

简化版

synchronized 提供互斥、可见性和可重入语义。同步代码块由 monitorenter/monitorexit 字节码表达,同步方法用 ACC_SYNCHRONIZED 标志;HotSpot 再通过对象头、轻量级锁记录和必要时膨胀的 ObjectMonitor 实现。所谓锁升级应按 JDK 版本理解:现代 HotSpot 已移除偏向锁,常见主线是无锁/轻量级竞争,竞争或等待需求出现时可能膨胀为重量级 Monitor,而不是永远背四级固定流水线。

详细版

同步代码块正常退出和异常退出都必须释放监视器,编译器会生成对应的异常处理路径。实例同步方法锁接收者 this,静态同步方法锁对应的 Class 对象;锁的是对象身份,不是某段代码文字。

HotSpot 在低竞争时会尽量走轻量级路径,通过原子操作和线程锁记录避免立即阻塞;竞争持续、调用 wait() 或实现需要时,锁可膨胀为 ObjectMonitor,等待者再被 park。偏向锁是旧优化,JDK 15 默认禁用,JDK 18 已 obsolete 并移除相关实现代码。锁的具体位图和状态转换属于 JVM 实现细节,不能当作 Java 语言规范。

解锁 happens-before 随后对同一监视器的加锁,因此 synchronized 不只保证同时一个线程进入临界区,还保证临界区写入对后续持锁线程可见。

完整版教学

一、语言语义先于 HotSpot 实现

Java 语言层面要求 synchronized 具备互斥和监视器内存语义,并允许同一线程重入。一个线程退出监视器 happens-before 另一个线程随后进入同一监视器,所以前者在临界区中的写入能被后者观察。

线程 A:lock M → 写 shared=42 → unlock M
                                  ↓ happens-before
线程 B:                 lock M → 读 shared,必须看到相应写入

这条保证不依赖面试者背出某一版 Mark Word 位图。对象头、CAS、锁记录和 ObjectMonitor 是 HotSpot 为实现语义采用的机制,其他 JVM 可以有不同布局。

二、代码块和同步方法在字节码层怎样表示

同步代码块通常编译为 monitorenter 和一条或多条 monitorexit。编译器必须覆盖正常返回和异常抛出路径,否则异常会把锁永久留住。同步方法没有显式包围整段方法的两条指令,而是在方法访问标志中带 ACC_SYNCHRONIZED,调用时由 JVM 隐式进入对应监视器。

写法锁对象字节码表达
synchronized(obj)obj 引用的对象monitorenter/monitorexit
实例同步方法当前实例 thisACC_SYNCHRONIZED
静态同步方法声明类的 Class 对象ACC_SYNCHRONIZED

两个实例分别调用同一个实例同步方法时,若锁对象不同就可以并发执行;静态同步方法则在同一个类加载器定义出的 Class 对象上竞争。

三、低竞争时为什么不必立刻挂起线程

阻塞和唤醒涉及调度,锁只被短时间占用时,立即 park 可能比等待锁释放更贵。HotSpot 因此会先尝试轻量级获取,使用 CAS 和线程相关锁记录表达持有关系;失败后还会根据竞争情况决定后续路径。

假设临界区只执行 2 微秒,而一次阻塞、调度、恢复合计可能达到几十微秒,轻量路径避免调度就有明显收益。但如果持锁 20 毫秒,长时间自旋会白白消耗 CPU,因此 JVM 不会无限自旋。

“轻量级”描述实现成本,不表示缺少互斥,也不承诺永不阻塞。自旋次数、锁记录布局和膨胀条件都会随 HotSpot 版本优化。

四、什么时候会使用 ObjectMonitor

竞争无法在轻量路径解决,或线程执行 Object.wait() 等需要等待集合的操作时,锁可能膨胀为 ObjectMonitor。Monitor 维护拥有者、进入竞争的线程以及 wait 集合等状态,未获锁线程可通过 park 让出 CPU。

竞争进入锁:Entry/竞争集合 → 获得 owner → 执行临界区
条件等待:  owner --wait()--> WaitSet --notify()--> 重新竞争 owner

notify() 只把某个等待者变为可重新竞争状态,不会把锁立即交出去;通知线程退出同步块后,等待者还要重新获得同一监视器。wait() 会释放监视器,而 sleep() 不会释放已经持有的锁。

五、锁状态演进必须绑定 JDK 版本

旧教程常背“无锁→偏向→轻量→重量”。偏向锁曾通过记录偏向线程来优化单线程反复加锁,但维护成本和现代负载收益变化使它在 JDK 15 默认禁用,JDK 18 进一步 obsolete 并移除实现代码。

现代 HotSpot 应重点理解轻量级锁与膨胀 Monitor,不应继续把偏向锁作为默认第一站。锁是否能去膨胀、何时回收 Monitor、对象头如何编码都属于版本实现细节,“所有升级终身绝不变化”也说得过满。

记忆钩子:规范只承诺互斥、可见性、重入和监视器行为;“几级锁、Mark Word 哪几位、能否降级”必须带上 HotSpot 与 JDK 版本。

六、可重入和异常安全怎样成立

同一线程再次进入已经持有的监视器不会把自己阻塞,运行时会维护重入层次。线程进入 3 次就必须匹配退出 3 次,最外层退出后其他线程才有机会获得锁。

Java 语法让正常返回和异常传播都自动执行监视器退出,这比手工锁更不容易漏解锁。它不意味着临界区中的业务资源会自动关闭,文件和连接仍需 try-with-resources

锁对象也必须稳定:若代码对可变字段 lock 加锁,另一个线程把字段换成新对象,两批线程可能分别锁住不同对象,互斥关系就被破坏。

七、竞争成本怎样观察和优化

假设 8 个线程都执行 1 ms 临界区,理论上同一把独占锁每秒最多完成约 1,000 次临界区,不会因有 8 个线程就变成 8,000 次。锁外工作、调度和缓存失效还会进一步降低实际吞吐。

现象可能原因优先动作
大量 BLOCKED临界区长或热点锁缩短锁内 I/O、拆分状态
CPU 高但吞吐低自旋/竞争、锁外重试用 JFR/剖析确认热点
尾延迟高持锁线程被慢调用拖住移出远程调用,建立超时
锁数量很多粒度过细、管理复杂评估分段收益和正确性

不要为了“无锁”牺牲不变量正确性。优化顺序应是先测量竞争,再减少锁内工作、降低共享、选择合适粒度,最后才比较其他同步器。

八、常见误区与追问

  • 误区:现代 synchronized 默认都先经过偏向锁。 偏向锁在 JDK 15 默认禁用,JDK 18 已 obsolete,现代 HotSpot 不应再套旧四级流程。
  • 误区:synchronized 锁住的是代码本身。 它锁的是求值得到的对象或 Class 对象,同一段代码使用不同锁对象并不互斥。
  • 误区:notify() 会把锁直接交给等待线程。 被通知线程只是重新参与竞争,通知者仍持锁到退出同步区域。
  • 追问:synchronized 怎样保证可见性? 对同一监视器的解锁 happens-before 后续加锁,JMM 据此规定相应写入可见。
  • 追问:同步方法和同步代码块字节码有什么区别? 前者使用 ACC_SYNCHRONIZED,后者通常显式出现 monitorenter/monitorexit
  • 追问:为什么 synchronized 可重入? 运行时识别拥有者并维护重入层次,同一线程重复进入不会与自己竞争。
  • 追问:虚拟线程遇到 synchronized 会怎样? JDK 24 的相关改进让常见监视器阻塞不再像早期实现那样固定 pin 载体线程,但本地代码等其他 pin 场景仍应按目标 JDK 诊断。

九、加强记忆

先记语义,再记实现:synchronized 通过同一监视器实现互斥、可见性和可重入,代码块对应 monitorenter/monitorexit,同步方法对应 ACC_SYNCHRONIZED。HotSpot 在低竞争时走轻量路径,竞争或 wait 需求出现时可膨胀为 ObjectMonitor,等待线程再 park;偏向锁属于已退出历史舞台的旧优化。notify 只让 WaitSet 中的线程重新竞争,不会立即移交锁。遇到“锁升级”追问时,明确区分 Java 规范与具体 JDK 实现,比机械背四级箭头更准确。