真实面经题目 · 原创解析

synchronized底层实现的原理?

synchronized 是 JVM 级别的内置同步机制。它通过 monitorenter/monitorexit 或方法访问标志进入对象监视器,基于对象头 Mark Word 记录锁状态,并在不同竞争程度下使用轻量级锁、重量级锁等实现互斥。它支持可重入,退出同步块会建立 happens-before 关系,也与 wait/notify 的条件等待机制绑定。

出现于:阿里巴巴 · 后端开发

60 秒回答模板

可以从字节码、对象结构、锁状态和内存语义四层回答。同步代码块会编译成 monitorenter 和 monitorexit 指令,线程进入时尝试获取对象关联的监视器,退出时释放监视器;同步方法不会显式出现这两个指令,而是通过方法的 ACC_SYNCHRONIZED 标志让 JVM 在方法调用和返回时自动加锁解锁。锁对象本身在对象头中有 Mark Word,里面会记录哈希、GC 年龄、锁标志位、线程或锁记录指针等信息。无竞争时 JVM 会尽量用轻量级方式完成加锁;出现竞争后会膨胀为重量级锁,线程阻塞和唤醒依赖操作系统调度。历史上还有偏向锁,但 JDK 15 起已废弃并默认关闭,现代 JDK 不应把偏向锁作为主线。synchronized 是可重入的,同一线程重复进入同一把锁会增加重入计数,退出到计数归零才真正释放。内存语义上,对一个锁的解锁 happens-before 后续线程对同一个锁的加锁,因此能保证同步块内写入对后续持锁线程可见。wait/notify/notifyAll 也依赖对象监视器,调用前必须持有对应锁,wait 会释放锁并进入等待队列,被通知后还要重新竞争锁。和 ReentrantLock 相比,synchronized 语法简单、异常安全、由 JVM 优化;ReentrantLock 提供可中断、公平锁、限时尝试加锁、多个 Condition 等更灵活能力,但需要手动释放锁。

考点 字节码入口
主线 对象监视器
易错点 把 synchronized 简单说成给方法加锁,没有…

深入解析

01

字节码入口

同步代码块会在字节码中生成 monitorenter 和 monitorexit。monitorenter 表示进入某个对象的监视器,monitorexit 表示退出并释放。编译器通常会为正常路径和异常路径都生成释放逻辑,保证同步块中抛异常时锁也能被释放。同步方法则不同,字节码中通常看不到 monitorenter/monitorexit,而是方法表上带 ACC_SYNCHRONIZED 标志,由 JVM 隐式获取和释放锁。

02

对象监视器

每个 Java 对象都可以作为锁,逻辑上都可以关联一个对象监视器。线程进入 synchronized 时,竞争的是锁对象关联的监视器。监视器内部维护持有者、重入计数、竞争队列、等待队列等状态。一个线程持有锁后,其他线程再进入同一把锁会进入竞争流程;如果执行 wait,则线程会释放当前监视器并进入等待队列。

03

对象头 Mark Word

HotSpot 对象头中的 Mark Word 是理解 synchronized 的关键。Mark Word 会随对象状态变化而复用存储不同信息,例如对象哈希、分代年龄、锁标志位、指向栈中 Lock Record 的指针、指向重量级监视器的指针等。加锁不是给对象额外挂一个布尔值,而是 JVM 通过 Mark Word 和运行时数据结构协作表达锁状态。

04

轻量级锁

轻量级锁主要服务于低竞争场景。线程进入同步块时,会在自己的栈帧中创建锁记录,并尝试通过 CAS 把对象 Mark Word 替换为指向该锁记录的指针。如果 CAS 成功,说明当前线程获得锁;如果发现锁已经由当前线程持有,则走可重入逻辑;如果 CAS 失败并存在真实竞争,JVM 会根据情况自旋或进入锁膨胀。

05

重量级锁

当多个线程对同一把锁发生明显竞争,轻量级方案无法低成本解决时,锁会膨胀为重量级锁。此时 Mark Word 会指向重量级监视器,竞争失败的线程可能被阻塞,后续唤醒依赖操作系统调度。重量级锁成本更高,因为涉及用户态和内核态切换、线程挂起与恢复,但在高竞争下比长时间自旋更合适。

06

偏向锁边界

偏向锁是较早 HotSpot 中针对同一线程反复进入同一把锁的优化,思想是把锁偏向某个线程,后续该线程进入时减少 CAS 成本。但这不是现代 synchronized 的主线。JDK 15 起偏向锁已被废弃并默认关闭,因此回答时可以把它作为历史优化补充说明,但不要把当前实现讲成主要依赖偏向锁。

07

可重入机制

synchronized 是可重入锁。同一线程已经持有某对象锁时,再次进入同一对象的 synchronized 代码不会死锁,而是增加重入层数或记录重入状态。只有该线程执行完所有嵌套同步区域,释放次数与进入次数匹配后,锁才真正对其他线程可用。

08

内存语义

synchronized 不只保证互斥,还保证可见性和有序性。Java 内存模型规定,对同一把锁的一次解锁 happens-before 后续线程对这把锁的一次加锁。也就是说,线程 A 在同步块内修改的共享变量,在 A 释放锁后,线程 B 再获得同一把锁时应当可见。

易错点

  • 把 synchronized 简单说成给方法加锁,没有说明锁的是对象监视器。
  • 说所有 synchronized 都会编译成 monitorenter/monitorexit,忽略同步方法使用 ACC_SYNCHRONIZED。
  • 把偏向锁当成现代 JDK 的主线实现,没有说明 JDK 15 起已废弃并默认关闭。
  • 只讲互斥,不讲 happens-before、可见性和有序性。
  • 认为 notify 后等待线程会立刻执行,忽略它还必须重新竞争锁。
  • 认为 wait 不释放锁,或把 wait 和 sleep 混为一谈。
  • 认为 synchronized 不可重入。
  • 把锁升级理解成一定按固定路径每次发生,忽略它是 JVM 根据竞争情况采取的优化策略。
  • 只比较 synchronized 和 ReentrantLock 的性能,不比较可中断、公平锁、tryLock、Condition、自动释放等能力差异。

面试官追问

为什么 wait 必须在 synchronized 中调用?

因为 wait 操作的是对象监视器的等待队列。线程必须先持有这个监视器,才能原子地释放锁并进入等待队列,否则会破坏等待和通知的同步关系,所以 JVM 会抛出 IllegalMonitorStateException。

synchronized 和 volatile 的区别是什么?

volatile 主要保证可见性和禁止特定重排序,不保证复合操作的互斥原子性;synchronized 同时保证互斥、可见性和有序性,适合保护多个变量或复合状态更新。

锁膨胀为什么有成本?

轻量级锁主要依赖 CAS 和线程栈中的锁记录,失败成本较低;重量级锁需要监视器对象、阻塞队列、线程挂起和唤醒,可能触发操作系统调度,因此上下文切换成本更高。

synchronized 是否一定比 ReentrantLock 慢?

不一定。现代 JVM 对 synchronized 有大量优化,在普通互斥场景下性能通常足够好。ReentrantLock 的优势主要是能力更丰富,而不是绝对更快。

同步方法锁的是什么?

实例同步方法锁 this,静态同步方法锁当前类的 Class 对象。同步代码块锁的是括号里表达式得到的对象。锁对象不同,互斥关系就不同。