synchronized 的实现原理是什么?

2025年 阅读约 12 分钟 面试指南 · Java面试 · Java并发

深入解析synchronized底层实现:对象头Mark Word结构、锁升级全过程(偏向锁→轻量级锁→重量级锁)、monitor对象原理、JDK 6+锁优化(锁消除、锁粗化、适应性自旋),附完整源码解读和面试模拟问答。

一句话总结

synchronized 在 JDK 6 之后引入了锁升级机制:无锁 → 偏向锁(记录线程 ID,同一线程重入无需 CAS)→ 轻量级锁(CAS 自旋,适用于竞争不激烈场景)→ 重量级锁(操作系统 mutex,线程阻塞唤醒)。锁的存储依赖对象头中的 Mark Word,重量级锁关联ObjectMonitor(_owner、_EntryList、_WaitSet)。JDK 6+ 还做了锁消除、锁粗化、适应性自旋等优化。

初级理解

synchronized 的三种使用方式

使用方式锁对象示例
修饰实例方法当前实例对象 thispublic synchronized void method()
修饰静态方法当前类的 Class 对象public static synchronized void method()
修饰代码块括号中指定的对象synchronized(obj) { ... }
public class SynchronizedDemo { private static int count = 0; private final Object lock = new Object(); // 实例方法锁 → 锁的是 this public synchronized void instanceMethod() { count++; } // 静态方法锁 → 锁的是 SynchronizedDemo.class public static synchronized void staticMethod() { count++; } // 代码块锁 → 锁的是 lock 对象 public void blockMethod() { synchronized (lock) { count++; } } }

对象头 — 锁存在哪里?

每个 Java 对象在堆内存中由三部分组成:

组成部分说明
对象头(Object Header)包含 Mark Word 和 Klass Pointer,锁信息存在 Mark Word 中
实例数据(Instance Data)对象的成员变量
对齐填充(Padding)保证对象大小是 8 字节的倍数

Mark Word 是对象头中的核心字段,32 位 JVM 占 32 bit,64 位 JVM 占 64 bit。它存储了对象的 hashCode、GC 分代年龄、锁状态标志等信息。锁的状态就记录在 Mark Word 的最后几位。

一句话总结:synchronized 可以锁方法或代码块,锁信息存储在对象头的 Mark Word 中。

中级深入

锁升级全过程

JDK 6 之后,synchronized 不再是直接使用重量级锁,而是有一个逐步升级的过程:

锁状态存储内容获取方式适用场景
无锁hashCode、分代年龄初始状态
偏向锁线程 ID、EpochCAS 设置线程 ID只有一个线程访问
轻量级锁指向栈中锁记录的指针CAS 自旋多线程交替访问
重量级锁指向 monitor 的指针操作系统 mutex多线程竞争激烈

偏向锁 — 为什么要有偏向锁?

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取。偏向锁就是为了在这个场景下减少不必要的 CAS 操作。

工作流程:

1. 线程第一次获取锁时,通过 CAS 将自己的线程 ID 写入 Mark Word

2. 之后该线程再次进入同步块时,只需检查 Mark Word 中的线程 ID 是否是自己,不需要任何 CAS 操作

3. 如果有其他线程竞争,偏向锁会撤销(在安全点暂停原线程,检查是否还在同步块中),升级为轻量级锁

// 偏向锁延迟:JVM 默认启动后 4 秒才启用偏向锁 // 因为 JVM 启动时会有大量同步操作,立即启用偏向锁会导致频繁撤销 // -XX:BiasedLockingStartupDelay=0 取消延迟 // -XX:-UseBiasedLocking 禁用偏向锁

轻量级锁 — CAS 自旋

当偏向锁被撤销或有第二个线程尝试获取锁时,升级为轻量级锁。

工作流程:

1. 线程在自己的栈帧中创建锁记录(Lock Record),存储 Mark Word 的副本

2. 通过 CAS 尝试将对象头的 Mark Word 替换为指向锁记录的指针

3. CAS 成功 → 获取轻量级锁成功

4. CAS 失败 → 检查 Mark Word 是否已经指向自己的锁记录(重入),是则成功;否则自旋等待

5. 自旋一定次数后仍未获取到锁 → 膨胀为重量级锁

// 轻量级锁加锁过程(简化) // 1. 在线程栈中创建 Lock Record LockRecord record = new LockRecord(); // 2. 拷贝对象头的 Mark Word 到 Lock Record record.setDisplacedMarkWord(obj.getMarkWord()); // 3. CAS 将对象头 Mark Word 替换为指向 Lock Record 的指针 while (!obj.casMarkWord(record.getDisplacedMarkWord(), ptrToRecord)) { // 4. CAS 失败,检查是否重入 if (obj.getMarkWord().pointsTo(record)) { // 重入成功,Lock Record 的 displaced mark word 设为 null record.setDisplacedMarkWord(null); break; } // 5. 自旋等待 spinCount++; if (spinCount > threshold) { // 膨胀为重量级锁 inflateToHeavyweightLock(obj); break; } }

重量级锁 — ObjectMonitor

当自旋失败或竞争激烈时,锁膨胀为重量级锁。重量级锁依赖操作系统的 mutex,线程会阻塞和唤醒(涉及用户态到内核态的切换,开销大)。

每个 Java 对象都可以关联一个 ObjectMonitor(C++ 实现),核心结构:

// HotSpot 源码中的 ObjectMonitor 结构(简化) class ObjectMonitor { void* _owner; // 持有锁的线程 Queue* _EntryList; // 等待获取锁的线程队列(BLOCKED 状态) Queue* _WaitSet; // 调用了 wait() 的线程队列(WAITING 状态) int _recursions; // 重入次数 int _count; // 锁计数器 }
字段作用
_owner指向持有锁的线程,null 表示锁空闲
_EntryList竞争锁失败的线程进入此队列,处于 BLOCKED 状态
_WaitSet调用 wait() 的线程进入此队列,处于 WAITING 状态,等待 notify()
_recursions记录重入次数,同一线程每次进入 +1,退出 -1,为 0 时释放锁
中级要点:锁升级路径是单向的(偏向→轻量→重量),不会降级。偏向锁减少同一线程的 CAS 开销,轻量级锁用 CAS 自旋避免阻塞,重量级锁用 mutex 处理激烈竞争。

高级拓展

锁消除(Lock Elimination)

JIT 编译器通过逃逸分析,发现某些同步代码不可能被多线程访问时,会直接去掉锁

// 锁消除示例 public String concat(String a, String b) { // StringBuffer 的 append 是 synchronized 的 StringBuffer sb = new StringBuffer(); sb.append(a); sb.append(b); return sb.toString(); } // sb 是局部变量,不会逃逸出方法 // JIT 会消除 append 方法上的 synchronized // 等价于 StringBuilder(无锁)的性能

锁粗化(Lock Coarsening)

如果一系列连续操作反复对同一个对象加锁解锁,JIT 会将锁的范围扩大(粗化),减少加锁解锁的次数。

// 锁粗化前:每次循环都加锁解锁 for (int i = 0; i < 100; i++) { synchronized (lock) { count++; } } // JIT 优化后等价于:只加一次锁 synchronized (lock) { for (int i = 0; i < 100; i++) { count++; } }

适应性自旋(Adaptive Spinning)

JDK 6 引入了适应性自旋:自旋时间不再固定,而是由前一次在同一个锁上的自旋时间锁的拥有者状态来决定。

  • 如果上次自旋成功获取了锁 → 这次自旋时间更长
  • 如果上次自旋失败 → 这次自旋时间更短,甚至不自旋直接阻塞

synchronized 和 ReentrantLock 对比

特性synchronizedReentrantLock
实现层面JVM 层面,C++ 实现JDK 层面,Java 实现(AQS)
锁释放自动释放(代码块结束或异常)必须手动 unlock(),通常在 finally 中
可中断不可中断,一直等待可中断(lockInterruptibly)
公平锁非公平支持公平和非公平(构造参数)
条件变量单一 wait/notify多个 Condition,精确唤醒
尝试获取不支持tryLock() 尝试获取,可设超时
性能(JDK 6+)经过大量优化,接近 ReentrantLock略优,但差距很小
// ReentrantLock 的 tryLock 和 Condition ReentrantLock lock = new ReentrantLock(); Condition notEmpty = lock.newCondition(); // 尝试获取锁,等 1 秒 if (lock.tryLock(1, TimeUnit.SECONDS)) { try { // 等待条件 while (queue.isEmpty()) { notEmpty.await(); } // 处理... } finally { lock.unlock(); } } // 另一个线程 lock.lock(); try { queue.add(item); notEmpty.signal(); // 精确唤醒等待 notEmpty 的线程 } finally { lock.unlock(); }

为什么说 synchronized 是"非公平锁"?

synchronized 的 _EntryList 中的线程被唤醒后,需要和新来的线程一起竞争锁,不会按等待顺序分配。新来的线程可能"插队"成功,这就是非公平的体现。

高级加分项:能说出锁消除依赖逃逸分析、锁粗化减少加锁频率、适应性自旋根据历史调整自旋时间、synchronized 底层是 ObjectMonitor 的 C++ 实现,说明你对 JVM 锁优化有深入理解。

实战场景

场景一:单例模式 — DCL 为什么用 volatile?

public class Singleton { // volatile 防止指令重排:分配内存 → 初始化 → 赋值引用 // 没有 volatile 时,赋值引用可能先于初始化完成 // 其他线程看到非 null 但未初始化的对象 private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { // 加锁 if (instance == null) { // 第二次检查 instance = new Singleton(); // 创建实例 } } } return instance; } }

场景二:用 synchronized 实现线程安全的计数器

public class SafeCounter { private int count = 0; public synchronized void increment() { count++; } public synchronized int get() { return count; } } // 注意:get() 也要加 synchronized // 否则可能读到过期值(synchronized 保证可见性)

常见坑

原因解决方案
锁了不同的对象实例方法锁 this,静态方法锁 Class,两个锁互不影响确保多线程操作的是同一个锁对象
锁了可变对象锁对象被修改后,其他线程锁的是新对象锁 final 对象或不可变对象
锁 String 常量字符串常量池共享,可能和其他模块冲突用 new Object() 作专用锁
getter 没加锁只给 setter 加锁,getter 可能读到中间状态读写都要加锁,或用 volatile
// 坑:锁了不同的对象 public class BadLock { public synchronized void methodA() {} // 锁 this public static synchronized void methodB() {} // 锁 BadLock.class // methodA 和 methodB 可以同时执行!它们锁的不是同一个对象 } // 坑:锁可变对象 Integer lock = 1; // Integer 是不可变的,但引用可变 synchronized (lock) { lock++; // lock++ 等价于 lock = Integer.valueOf(lock.intValue() + 1) // 此时 lock 指向了新对象!下次 synchronized(lock) 锁的是另一个对象 }

面试模拟

Q:synchronized 的锁升级过程是怎样的?

A:JDK 6 之后引入了锁升级机制,锁状态存在对象头的 Mark Word 中。升级路径是单向的:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。偏向锁在 Mark Word 中记录线程 ID,同一线程重入无需 CAS;轻量级锁通过 CAS 自旋获取,适用于线程交替执行的场景;自旋失败次数达到阈值后膨胀为重量级锁,依赖操作系统的 mutex,线程会阻塞。此外还有锁消除、锁粗化、适应性自旋等 JIT 优化。

Q:偏向锁什么时候会被撤销?

A:偏向锁在两种情况下会被撤销:1)有其他线程竞争锁时,会在安全点暂停偏向线程,检查它是否还在同步块中,如果已退出则撤销偏向锁并升级为轻量级锁,如果还在执行则升级为轻量级锁让竞争线程自旋;2)调用对象的 hashCode() 时,因为偏向锁的 Mark Word 没有空间存储 hashCode(偏向线程 ID 占用了 hashCode 的位置),会撤销偏向锁并计算 hashCode 存入 Mark Word。

Q:synchronized 和 ReentrantLock 怎么选?

A:JDK 6 之后 synchronized 性能已经和 ReentrantLock 非常接近。选择原则:1)优先用 synchronized,代码简洁,自动释放锁,不容易出错;2)需要可中断获取锁、超时获取锁、公平锁、多个条件变量时用 ReentrantLock;3)synchronized 是 JVM 内置的,一些监控工具(如 jstack)可以直接看到锁信息,排查问题更方便。

Q:wait() 为什么要放在 while 循环里而不是 if?

A:因为虚假唤醒(spurious wakeup):线程可能在没被 notify() 的情况下被唤醒。如果用 if 判断条件,被虚假唤醒后会直接往下执行,可能操作一个不满足条件的状态。用 while 循环可以在被唤醒后再次检查条件,条件不满足则继续 wait()。

Q:synchronized 修饰静态方法和实例方法的区别?

A:实例方法锁的是 this 对象,同一个实例的多个 synchronized 实例方法互斥;静态方法锁的是类的 Class 对象,同一个类的所有 synchronized 静态方法互斥。但实例方法和静态方法之间不互斥,因为它们锁的是不同的对象(this vs Class)。