少女祈祷中...
NEKO
知识库

java 可见性面试要点

一、可见性

Q1:什么是可见性?为什么需要它?

答: 可见性指一个线程修改了共享变量,其他线程能立即看到。

问题根源:现代 CPU 有多级缓存,每个线程可能读取到缓存中的旧值:

线程 A          线程 B
  |               |
CPU缓存A        CPU缓存B
  |               |
  +---- 主内存 ----+

// 线程 B 可能永远看不到 flag 的变化
static boolean flag = false;

// 线程 A
flag = true;

// 线程 B
while (!flag) { } // 死循环

volatile 解决:强制读写主内存,保证可见性。


Q2:volatile 如何保证可见性?

答: 通过 内存屏障 + 缓存一致性协议:

  1. 写 volatile 变量:

    • 在写操作后插入 StoreLoad 屏障

    • 强制把缓存刷回主内存

    • 发送总线消息,让其他 CPU 缓存失效

  2. 读 volatile 变量:

    • 在读操作前插入 LoadLoad 屏障

    • 强制从主内存重新加载

// 底层:lock 前缀指令
// 1. 将当前处理器缓存行写回内存
// 2. 使其他 CPU 缓存该地址失效(MESI 协议)

本质:volatile 变量的读写都会绕过 CPU 缓存,直接操作主内存。


Q3:volatile 和 synchronized 在可见性上有什么区别?

答:

对比项

volatile

synchronized

范围

单个变量

代码块/方法

原子性

不保证

保证

阻塞

不阻塞

阻塞

性能

更轻量

较重

可见性

保证

保证

// volatile:只保证可见性
volatile int count = 0;

// synchronized:可见性 + 原子性
synchronized void increment() { count++; }

核心:synchronized 通过 Monitor 的 monitorenter/exit 也保证可见性(释放锁时刷回主内存)。


Q4:哪些操作会保证可见性?

答:

  1. volatile 读写

  2. synchronized 释放/获取锁

  3. final 字段初始化(安全发布)

  4. 线程启动/终止:start() 前的操作对子线程可见

  5. 中断:interrupt() 前的操作对被中断线程可见

  6. 并发工具:Atomic、Lock、CountDownLatch 等

// 线程启动保证可见性
int x = 1;
new Thread(() -> {
    System.out.println(x); // 一定输出 1
}).start();

Q5:为什么 volatile 能保证可见性,但普通变量不行?

答: 普通变量读写不经过内存屏障:

// 普通变量
int x = 1;   // 可能只写入 CPU 缓存
int y = x;   // 可能只从 CPU 缓存读

CPU 缓存层级:

寄存器 → L1 → L2 → L3 → 主内存
  • 普通变量:可能在 L1/L2 缓存,其他核心看不到

  • volatile:强制刷到主内存,并让其他缓存失效

本质:volatile 用性能换可见性。


Q6:volatile 能保证数组元素的可见性吗?

答: 不能。volatile 只保证引用本身的可见性:

volatile int[] arr = new int[10];

arr[0] = 1; // 不保证可见性!
arr = new int[20]; // 保证引用可见

解决:

  • 用 AtomicIntegerArray

  • 或用 synchronized

AtomicIntegerArray arr = new AtomicIntegerArray(10);
arr.set(0, 1); // 保证可见性 + 原子性

Q7:单例模式的双重检查锁为什么要加 volatile?

答: 防止指令重排导致拿到半初始化对象:

public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {              // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {      // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

不加 volatile 的问题:

// new Singleton() 实际分三步:
// 1. 分配内存
// 2. 初始化对象
// 3. instance 指向内存

// 重排后可能变成 1→3→2
// 线程 B 在 3 之后、2 之前进入,拿到 null 对象的引用

volatile 禁止 2 和 3 重排,保证安全发布。


二、禁止指令重排

Q8:什么是指令重排?

答: 编译器、CPU、内存系统为了优化性能,会改变指令执行顺序:

// 源码
int a = 1;
int b = 2;
int c = a + b;

// 可能重排为
int b = 2;
int a = 1;
int c = a + b;

重排类型:

  1. 编译器重排:编译期优化

  2. CPU 重排:乱序执行

  3. 内存重排:缓存和写缓冲导致

原则:不改变单线程执行结果(as-if-serial)。


Q9:指令重排有什么问题?

答: 多线程下,重排可能导致逻辑错误:

// 经典案例:DCL 单例
instance = new Singleton();
// 重排后:先赋值引用,再初始化
// 其他线程拿到未初始化对象

// 案例2:可见性问题
int x = 0, y = 0;
// 线程1        线程2
x = 1;         y = 1;
int r1 = y;    int r2 = x;
// 可能 r1=0, r2=0(都重排了)

根本原因:重排在单线程无影响,但多线程下破坏有序性。


Q10:volatile 如何禁止指令重排?

答: 通过插入内存屏障:

屏障

作用

LoadLoad

禁止读读重排

StoreStore

禁止写写重排

LoadStore

禁止读写重排

StoreLoad

禁止写读重排(最重)

volatile 写:

StoreStore 屏障
volatile 写
StoreLoad 屏障

volatile 读:

LoadLoad 屏障
volatile 读
LoadStore 屏障

// JMM 规定的 volatile 重排规则
// 1. volatile 写之前的操作,不能重排到写之后
// 2. volatile 读之后的操作,不能重排到读之前
// 3. volatile 写不能与 volatile 读重排

Q11:什么是 happens-before 原则?

答: JMM 定义的偏序关系,保证可见性和有序性:

规则

说明

程序顺序

同一线程,前操作 hb 后操作

监视器锁

解锁 hb 后续加锁

volatile

写 hb 后续读

线程启动

start() hb 子线程所有操作

线程终止

线程所有操作 hb join() 返回

中断

interrupt() hb 检测到中断

传递性

A hb B,B hb C,则 A hb C

// volatile 写 hb 读
volatile int x;
x = 1;        // 写
int y = x;    // 读,一定看到 1

核心:happens-before 是判断数据竞争的依据。


Q12:volatile 能保证有序性吗?

答: 能,但有条件:

  • volatile 变量的读写之间有序

  • volatile 写之前的操作,不会重排到写之后

  • volatile 读之后的操作,不会重排到读之前

int a = 0;
volatile boolean flag = false;

// 线程 A
a = 1;           // 1
flag = true;     // 2,volatile 写
// 保证 1 在 2 之前执行

// 线程 B
if (flag) {      // 3,volatile 读
    int b = a;   // 4
    // 保证 4 能看到 a=1
}

关键:volatile 建立跨线程的 happens-before 关系。


Q13:为什么 volatile 写比读开销大?

答:

  • volatile 读:插入 LoadLoad + LoadStore 屏障,开销小

  • volatile 写:插入 StoreStore + StoreLoad 屏障

StoreLoad 屏障最重:

  • 禁止写读重排

  • 需要刷新写缓冲区

  • 可能触发缓存一致性协议(MESI)

// StoreLoad 屏障伪代码
storeStoreBarrier();
volatileWrite();
storeLoadBarrier(); // 最耗时

结论:读多写少场景,volatile 性能可接受。


Q14:as-if-serial 和 happens-before 有什么区别?

答:

对比项

as-if-serial

happens-before

范围

单线程

多线程

目的

保证单线程结果不变

保证多线程可见性/有序性

重排

允许,只要结果不变

约束特定操作顺序

层次

编译器/CPU

JMM 规范

关系:as-if-serial 允许重排,happens-before 限制重排。

注意:as-if-serial 不保证多线程正确性。


三、不保证原子性

Q15:什么是原子性?

答: 原子性指一个操作不可分割,要么全执行,要么全不执行:

int x = 1;      // 原子(基本类型赋值)
x++;            // 非原子(读-改-写三步)
long y = 1L;    // 32位 JVM 上非原子

x++ 实际步骤:

  1. 读取 x

  2. 计算 x+1

  3. 写回 x

多线程下可能丢失更新。


Q16:为什么 volatile 不保证原子性?

答: volatile 只保证可见性和有序性,不保证复合操作的原子性:

volatile int count = 0;

// 10 个线程各执行 1000 次
count++; // 非原子!

问题:

  1. 线程 A 读 count=0

  2. 线程 B 读 count=0

  3. A 写 count=1

  4. B 写 count=1(丢失 A 的更新)

结果:最终 count < 10000。


Q17:如何验证 volatile 不保证原子性?

答:

public class VolatileTest {
    private static volatile int count = 0;

    public static void main(String[] args) 
            throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    count++;
                }
            });
            threads[i].start();
        }
        for (Thread t : threads) t.join();
        System.out.println(count); // 通常 < 10000
    }
}

结果:几乎总是小于 10000,证明 volatile 不保证原子性。


Q18:如何保证原子性?

答: 三种方式:

  1. synchronized:

synchronized void increment() { count++; }
  1. Lock:

lock.lock();
try { count++; } finally { lock.unlock(); }
  1. 原子类:

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 保证原子

推荐:低竞争用原子类,高竞争用 LongAdder,复杂逻辑用锁。


Q19:volatile 适用哪些场景?

答:

  1. 状态标志:

volatile boolean running = true;
while (running) { /* 工作 */ }
  1. 双重检查锁:

private static volatile Singleton instance;
  1. 一次性安全发布:

volatile Config config;
config = loadConfig(); // 发布
  1. 独立观察:

volatile int temperature; // 定期更新,其他线程读取

不适用:

  • 复合操作(count++)

  • 依赖当前值的操作

  • 多变量一致性


Q20:volatile 和 AtomicInteger 如何选择?

答:

场景

推荐

单一状态标志

volatile

计数器

AtomicInteger / LongAdder

复合操作

原子类或锁

多变量

锁

读多写少

volatile

高并发计数

LongAdder

// 标志位:volatile
volatile boolean shutdown = false;

// 计数:AtomicInteger
AtomicInteger count = new AtomicInteger(0);

Q21:volatile 能替代锁吗?

答: 不能完全替代:

能力

volatile

锁

可见性

✅

✅

有序性

✅(有限)

✅

原子性

❌

✅

互斥

❌

✅

阻塞

❌

✅

结论:

  • 简单标志位可用 volatile

  • 复合操作、临界区必须用锁

  • volatile 是轻量级同步,不是锁的替代品


Q22:为什么 long/double 的读写可能不是原子的?

答: 在 32 位 JVM 上:

  • long/double 是 64 位

  • 需要两次 32 位操作

  • 可能读到"半个"值

// 32 位 JVM 上非原子
long x = 0;
// 线程 A         线程 B
x = 0xFFFFFFFFL;  long y = x; // 可能读到中间值

解决:

  • 用 volatile long(保证原子性)

  • 用 AtomicLong

注意:64 位 JVM 上 long/double 读写是原子的(JLS 不保证,但实现如此)。


Q23:volatile 和 final 有什么关系?

答: final 保证安全发布:

public class Config {
    final int x;
    final int y;

    public Config() {
        x = 1;
        y = 2;
    }
}

JMM 保证:

  • final 字段在构造完成后,对其他线程可见

  • 禁止 final 字段的写重排到构造外

// 其他线程读到 config 引用时
// 一定能看到 x=1, y=2
Config config = new Config();

volatile + final:常用于不可变对象的安全发布。


Q24:什么是内存屏障?volatile 用了哪些?

答: 内存屏障是 CPU 指令,禁止特定重排:

屏障

作用

LoadLoad

禁止读读重排

StoreStore

禁止写写重排

LoadStore

禁止读写重排

StoreLoad

禁止写读重排

volatile 写的屏障:

StoreStore
volatile 写
StoreLoad

volatile 读的屏障:

LoadLoad
volatile 读
LoadStore

x86 架构:只有 StoreLoad 需要显式指令(mfence),其他是空操作。


Q25:JMM 是什么?

答: JMM(Java Memory Model)是多线程内存模型规范:

  • 定义线程和主内存的抽象关系

  • 规定可见性、有序性、原子性规则

  • 屏蔽不同硬件的差异

核心内容:

  1. 主内存 + 工作内存抽象

  2. happens-before 原则

  3. 内存屏障指令

  4. volatile、synchronized、final 语义

线程 A        线程 B
工作内存A     工作内存B
   |             |
   +--- 主内存 ---+

Q26:volatile 的底层实现是什么?

答: 通过 lock 前缀指令:

volatile int x = 1;
// 编译后:
// mov [x], 1
// lock addl $0, (%rsp)  ; 内存屏障

lock 指令的作用:

  1. 将当前缓存行写回内存

  2. 使其他 CPU 的缓存行失效(MESI)

  3. 禁止指令重排

MESI 协议:

  • Modified:已修改

  • Exclusive:独占

  • Shared:共享

  • Invalid:失效

本质:volatile 用缓存一致性协议保证可见性。


Q27:volatile 和 CAS 如何配合?

答: CAS 保证原子性,volatile 保证可见性:

public class AtomicInteger {
    private volatile int value; // 可见性

    public final boolean compareAndSet(int expect, int update) {
        // Unsafe 的 CAS 保证原子性
        return U.compareAndSwapInt(this, VALUE, expect, update);
    }
}

组合:

  • volatile:保证 value 的可见性

  • CAS:保证读-改-写的原子性

这是所有原子类的基础。


Q28:volatile 在并发工具中的应用?

答:

  1. AQS 的 state:

private volatile int state;
  1. ConcurrentHashMap 的节点:

volatile V val;
volatile Node<K,V> next;
  1. ThreadPoolExecutor 的 ctl:

private final AtomicInteger ctl = ...;
// 内部用 volatile + CAS
  1. CopyOnWriteArrayList 的 array:

private transient volatile Object[] array;

核心:volatile 是无锁并发的基础。


Q29:volatile 能保证 64 位原子性吗?

答: 能。JLS 规定:

  • volatile long/double 的读写是原子的

  • 普通 long/double 在 32 位 JVM 上不保证

volatile long x = 0; // 原子读写

原理:volatile 的内存屏障保证 64 位操作不被拆分。

注意:volatile 只保证读写原子,x++ 仍非原子。


Q30:volatile 的最佳实践是什么?

答:

  1. 仅用于状态标志:

volatile boolean running = true;
  1. DCL 单例必须加:

private static volatile Singleton instance;
  1. 不要用于复合操作:

volatile int count = 0;
count++; // 错误!
  1. 配合 CAS 使用:

AtomicInteger count = new AtomicInteger(0);
  1. 优先用现成工具:

AtomicBoolean、LongAdder、ConcurrentHashMap

四、综合对比

Q31:volatile、synchronized、Atomic 三者对比?

答:

对比项

volatile

synchronized

Atomic

可见性

✅

✅

✅

有序性

✅

✅

✅

原子性

❌

✅

✅

阻塞

否

是

否

性能

高

低

高

范围

变量

代码块

变量

死锁

无

可能

无

选择:

  • 状态标志:volatile

  • 简单计数:Atomic

  • 复杂临界区:synchronized


Q32:为什么说 volatile 是轻量级同步?

答:

  • 不涉及线程阻塞和上下文切换

  • 只插入内存屏障

  • 开销远小于锁

但不是万能:

  • 不保证原子性

  • 不能替代锁

  • 只适用于特定场景

// 轻量:volatile
volatile boolean flag;

// 重量:synchronized
synchronized (lock) { ... }

Q33:volatile 的读写性能如何?

答:

  • 读:几乎无开销(x86 上 LoadLoad/LoadStore 是空操作)

  • 写:有开销(StoreLoad 屏障,mfence 指令)

测试结论:

  • 读多写少:volatile 性能接近普通变量

  • 频繁写:volatile 比普通变量慢

建议:读多写少场景放心用 volatile。


Q34:volatile 能防止死锁吗?

答: 不能。volatile 不涉及锁,也就无所谓死锁。

但 volatile 可辅助避免死锁:

  • 状态标志 + CAS 重试

  • 无锁编程

// 无锁栈(Treiber Stack)
AtomicReference<Node> top = new AtomicReference<>();

注意:无锁不等于无阻塞,可能活锁。


Q35:volatile 的内存语义是什么?

答: JMM 规定的 volatile 语义:

  1. 写语义:

    • 写 volatile 时,把工作内存刷新到主内存

    • 等价于释放锁

  2. 读语义:

    • 读 volatile 时,从主内存重新加载

    • 等价于获取锁

总结:

volatile 写 ≈ 解锁
volatile 读 ≈ 加锁

这是理解 volatile 的关键。

评论