1. synchronized 的底层原理?锁升级的过程?

synchronized 三兄弟:同步代码块(monitorenter/monitorexit 字节码指令)、同步方法(ACC_SYNCHRONIZED 标志)、static 方法(锁 Class 对象,实例方法锁 this)。本质都是监视器锁 Monitor。

锁升级(JDK6 引入,从无锁到重量级):

  1. 无锁:无竞争状态
  2. 偏向锁:只有一个线程反复进入同步块。Mark Word 记录线程 ID,再次进入直接判断,无需 CAS。竞争时撤销偏向锁
  3. 轻量级锁:多个线程交替进入(无真实竞争)。通过 CAS 把 Mark Word 拷贝到栈帧锁记录,失败则膨胀。自旋等待(默认自旋 10 次)
  4. 重量级锁:真实竞争激烈。升级为 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 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 不保证真实执行顺序,只保证观察结果)。核心规则:

  1. 程序顺序规则:同一线程内,前面的操作 happens-before 后面的
  2. 锁规则:解锁 happens-before 后续的加锁(同一把锁)
  3. volatile 规则:volatile 写 happens-before 后续对该变量的读
  4. 传递性:A happens-before B,B happens-before C → A happens-before C
  5. 线程启动规则(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 是如何实现禁止重排的?(内存屏障)

CPU 层面:现代 CPU 有写缓冲(Store Buffer)和失效队列(Invalidate Queue),内存屏障强制刷新/等待,保证多核缓存一致性(MESI 协议配合)。这也是 volatile 有轻微性能损耗的原因。

🎯 面试要点

  • JMM 层面屏障:LoadLoad / LoadStore / StoreStore / StoreLoad 四种
  • volatile 读≈加锁读(无锁语义:Lock前缀指令/内存屏障)
  • 深入可提:x86 是强内存模型(TSO),实际只有 StoreLoad 有成本;ARM 弱模型需要更多屏障

🎤 常见面试追问

  1. 锁信息存在哪里?——对象头 Mark Word 的锁标志位(2 bit):01 偏向锁、00 轻量级、10 重量级、11 GC 标记。
  2. 锁升级是不可逆的吗?——基本不可逆(偏向锁可批量撤销降级),轻量级→重量级后不会再降回——所以设计上"尽量无竞争"比"事后升级"重要。
  3. volatile 能替代 synchronized 吗?——不能完全替代:volatile 保证可见性+有序性,不保证原子性(count++ 用 volatile 照样错);读多写一且不依赖旧值的场景才适合。
  4. DCL 单例为什么必须 volatile?——new 对象分三步(分配内存/构造/赋值),可能重排成"先赋值后构造";另一线程看到非 null 就返回了未构造完的对象。volatile 禁止重排。
  5. JDK15 为什么默认禁用偏向锁?——偏向锁撤销有成本(STW 级别),现代应用线程竞争普遍,收益变小;JEP 374 默认禁用并计划废弃。

📖 名词解释(本页术语)

术语 大白话解释
synchronized内置锁关键字:同一时刻只允许一个线程进入被锁代码块(互斥),同时保证可见性。锁的是对象/类。
Monitor(监视器)synchronized 的底层实现机制:每个对象关联一个 monitor(管程),管着"谁能进、谁在等"。
偏向锁 / 轻量级锁 / 重量级锁锁的三档升级:偏向锁(只有一个线程,几乎零开销)→ 轻量级锁(交替访问,CAS 自旋)→ 重量级锁(真竞争,线程阻塞)。
Mark Word对象头里 8 字节,存储哈希/GC 年龄/锁状态——锁升级的信息都写在它上面。
volatile关键字:保证变量"写后立即可见 + 禁止重排",不保证原子性。适合状态标志、DCL 单例。
内存屏障CPU/编译器指令,禁止屏障两侧的指令重排并强制缓存刷新——volatile 的实现机制。
happens-beforeJMM 给程序员的"可见性承诺"规则集(锁规则/volatile 规则/传递性等):满足规则的操作,前者的结果对后者可见。
CAS(Compare And Swap)无锁原子操作:比较"内存值 == 期望值"才更新为新值,失败重试。轻量级锁、原子类都用它。
⚠️ 本页面由 AI 生成,内容仅供参考,请以官方文档和实际源码为准。