JVM 对 Synchronized 关键字的实现是怎样的?

#Java#面试八股

JVM 对 Synchronized 关键字的实现是怎样的?

结论先行:synchronized 在字节码层面是两条指令——同步代码块编译为 monitorenter / monitorexit(编译器保证异常路径也有一次 monitorexit),同步方法则在方法访问标志里打上 ACC_SYNCHRONIZED。运行时真正干活的是对象监视器(monitor,HotSpot 里是 ObjectMonitor),它依赖对象头 Mark Word 与等待队列工作,并按「偏向锁 → 轻量级锁 → 重量级锁」逐级升级。

一、字节码证据

public class SyncDemo {
    private final Object lock = new Object();
    private int count;

    public void block() {                 // 编译后是 monitorenter / monitorexit
        synchronized (lock) {
            count++;
        }
    }

    public synchronized void method() {   // 编译后方法标志带 ACC_SYNCHRONIZED
        count++;
    }
}
javap -c -v SyncDemo.class | sed -n '/void block/,/^$/p'
# 关键片段:
#   aload_0
#   getfield  #2   // lock
#   dup
#   astore_1
#   monitorenter
#   ...            业务代码
#   aload_1
#   monitorexit
#   ...           异常路径 handler 中还有一次 monitorexit
#   athrow

javap -v 里 method() 的访问标志会显示 ACC_PUBLIC, ACC_SYNCHRONIZED,由 JVM 在方法调用时隐式加锁解锁。

二、对象头与锁状态

锁状态Mark Word 标志位开销适用场景
无锁0010单线程访问
偏向锁101一次 CAS 记录线程 ID同一线程反复进入(JDK 15 起默认禁用)
轻量级锁00栈帧锁记录 + CAS 自旋多线程交替执行、竞争短
重量级锁10操作系统互斥量,线程阻塞挂起竞争激烈、持锁时间长
竞争升级路径:
单线程进入 ──偏向锁(记 ThreadID)──→ 另一线程竞争
                                        │ CAS 替换 Mark Word 失败
                                        ▼
                              轻量级锁(栈上 Lock Record + 自旋)
                                        │ 自旋超阈值 / 竞争加剧
                                        ▼
                              重量级锁(ObjectMonitor + 等待队列)

升级不可逆(除偏向锁可撤销),因为重量级锁的实现成本最高、回退没有收益。

三、ObjectMonitor 的结构

字段作用
_owner当前持有锁的线程
_recursions重入次数,对应可重入语义
_EntryList阻塞等待获取锁的线程队列
_WaitSet调用 wait() 后进入的等待集合
_cxq竞争激烈的线程先入此单向队列

wait() 会把线程放入 _WaitSet 并释放锁,notify() 把它移回 _EntryList 重新竞争;notifyAll() 则是全部移回。这也是为什么 wait / notify 必须在 synchronized 块中调用——没有 monitor 就没有这两个队列。

四、相关参数与观察

# JDK 8:偏向锁默认开启,启动后延迟 4 秒生效
java -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -jar app.jar

# JDK 15+:偏向锁被废弃,参数只告警不生效
java -XX:+UseBiasedLocking -jar app.jar
# OpenJDK 64-Bit Server VM warning: Option UseBiasedLocking was deprecated

# 打印锁的膨胀与撤销(需 debug 版 JVM 才完整)
java -XX:+PrintFlagsFinal -version | grep -i biased

五、常见追问

追问回答要点
为什么 monitorexit 有两个一条对应正常退出,一条在异常处理器里,保证抛异常也能释放锁
偏向锁为什么被废弃撤销成本高(需 safepoint)、现代应用竞争普遍、收益被 G1/ZGC 与更快的 CAS 抵消
锁消除和锁粗化是什么JIT 优化的两种:前者删掉不可能竞争的锁(如局部 StringBuffer),后者把循环内的锁合并到循环外
synchronized 与 ReentrantLock 底层差异前者是 JVM 内置 monitor,后者是 AQS 的 state + 队列,前者不可中断、无公平模式
重量级锁一定慢吗不一定,竞争激烈时阻塞挂起比自旋空转更省 CPU;自旋只适合短临界区

小结

字节码层是 monitorenter / monitorexit 与 ACC_SYNCHRONIZED,运行期是 Mark Word 加 ObjectMonitor 的锁升级。把「指令 → 对象头 → 监视器 → 升级路径」四层讲清,这道题就答到位了。

笔记加载中…