🔒 synchronized & volatile
锁升级全过程 · 对象头 Mark Word · volatile 可见性与指令重排 · happens-before
1. synchronized 的底层原理?锁升级的过程?
synchronized 三兄弟:同步代码块(monitorenter/monitorexit 字节码指令)、同步方法(ACC_SYNCHRONIZED 标志)、static 方法(锁 Class 对象,实例方法锁 this)。本质都是监视器锁 Monitor。
锁升级(JDK6 引入,从无锁到重量级):
- 无锁:无竞争状态
- 偏向锁:只有一个线程反复进入同步块。Mark Word 记录线程 ID,再次进入直接判断,无需 CAS。竞争时撤销偏向锁
- 轻量级锁:多个线程交替进入(无真实竞争)。通过 CAS 把 Mark Word 拷贝到栈帧锁记录,失败则膨胀。自旋等待(默认自旋 10 次)
- 重量级锁:真实竞争激烈。升级为 Monitor(依赖 OS 互斥量),未抢到锁的线程进入 阻塞(用户态↔内核态切换,代价大)
锁升级是主流路径,但并非严格不可逆:轻量级锁释放后若已无竞争,可以撤销回无锁状态(deflate);偏向锁也可批量撤销。JDK 15 起默认禁用偏向锁(JEP 374)。
反编译看 synchronized 字节码
public void incr() {
synchronized (this) { count++; }
}
// javap -c 输出(关键指令)
0: aload_0
1: dup
2: monitorenter // 获取监视器锁
3: aload_0
4: dup
5: getfield count
8: iconst_1
9: iadd
10: putfield count
13: aload_1
14: monitorexit // 释放锁
🎯 面试要点
- 锁信息存在对象头 Mark Word 中(2 bit 锁标志位:01 = 无锁或偏向(靠偏向位区分)、00 = 轻量级、10 = 重量级、11 = GC 标记)
- 锁升级是"在锁竞争加剧时逐步增重",用空间换性能
- 可重入:同一线程再次进入同一把锁会成功——重入次数不在 Mark Word 里:偏向锁靠线程 ID + epoch 隐式判断,轻量级锁靠栈上多个 Lock Record,重量级锁靠
ObjectMonitor._recursions
2. volatile 关键字的作用?能保证原子性吗?
两大作用:
- 可见性:volatile 变量写操作后会强制刷新到主内存,读操作从主内存读(实现:插入内存屏障,禁止缓存)。线程 A 修改立即可见
- 有序性(禁止指令重排):volatile 写前后加屏障,防止重排到屏障外
不保证原子性:volatile int i; i++ 依然是"读-改-写"三步,两个线程可以都读到同一个旧值。所以 volatile 适合:状态标志位、单次写(发布引用)、double-check 单例。
volatile 的经典应用:双重检查锁单例
class Singleton {
private static volatile Singleton instance; // volatile 关键!
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(有锁)
instance = new Singleton(); // ①分配内存 ②构造 ③赋值
}
}
}
return instance;
}
}
// 不加 volatile 的坑:②③可能重排为①③②,另一线程检查 instance != null 返回了未构造完的对象
🎯 面试要点
- volatile 解决的是可见性 + 有序性,不解决原子性(对比 synchronized 三者全解决)
- DCL 单例必须 volatile——这是最经典的追问点
- 轻量级替代:AtomicBoolean 等原子类内部也是 volatile + CAS
3. 什么是 happens-before 原则?
定义:如果操作 A happens-before 操作 B,则 A 的执行结果对 B 可见,且 A 的执行顺序在 B 之前(JMM 不保证真实执行顺序,只保证观察结果)。核心规则:
- 程序顺序规则:同一线程内,前面的操作 happens-before 后面的
- 锁规则:解锁 happens-before 后续的加锁(同一把锁)
- volatile 规则:volatile 写 happens-before 后续对该变量的读
- 传递性:A happens-before B,B happens-before C → A happens-before C
- 线程启动规则(start)、线程终止规则(join)、中断规则、对象终结规则(finalize)
这些规则是 JMM 用来判断可见性是否成立的依据:可见性仍然由同步动作提供——两个线程之间如果没有 volatile 写读、锁的释放获取、start/join 等同步点,就不存在跨线程的 happens-before 边,切不可理解成「不加同步也安全」。
🎯 面试要点
- 经典例子:线程 A 修改变量后调用 threadB.start() → A 的修改对 B 可见(start 规则)
- 线程 A 执行 t.join() → A 能看到 t 线程内的所有修改
- Happens-before 是面试深水区:答出传递性 + 锁规则 + volatile 规则即可得分
4. volatile 是如何实现禁止重排的?(内存屏障)
- 写屏障(StoreStore + StoreLoad):volatile 写前插 StoreStore(防止前面普通写重排到写后面),写后插 StoreLoad(防止写重排到后面的读)
- 读屏障(LoadLoad + LoadStore):volatile 读后插 LoadLoad/LoadStore,防止后续普通读写重排到读前面
CPU 层面:现代 CPU 有写缓冲(Store Buffer)和失效队列(Invalidate Queue),内存屏障强制刷新/等待,保证多核缓存一致性(MESI 协议配合)。这也是 volatile 有轻微性能损耗的原因。
🎯 面试要点
- JMM 层面屏障:LoadLoad / LoadStore / StoreStore / StoreLoad 四种
- volatile 读≈加锁读(无锁语义:Lock前缀指令/内存屏障)
- 深入可提:x86 是强内存模型(TSO),实际只有 StoreLoad 有成本;ARM 弱模型需要更多屏障