一、可见性
Q1:什么是可见性?为什么需要它?
答: 可见性指一个线程修改了共享变量,其他线程能立即看到。
问题根源:现代 CPU 有多级缓存,每个线程可能读取到缓存中的旧值:
线程 A 线程 B
| |
CPU缓存A CPU缓存B
| |
+---- 主内存 ----+// 线程 B 可能永远看不到 flag 的变化
static boolean flag = false;
// 线程 A
flag = true;
// 线程 B
while (!flag) { } // 死循环volatile 解决:强制读写主内存,保证可见性。
Q2:volatile 如何保证可见性?
答: 通过 内存屏障 + 缓存一致性协议:
写 volatile 变量:
在写操作后插入 StoreLoad 屏障
强制把缓存刷回主内存
发送总线消息,让其他 CPU 缓存失效
读 volatile 变量:
在读操作前插入 LoadLoad 屏障
强制从主内存重新加载
// 底层:lock 前缀指令
// 1. 将当前处理器缓存行写回内存
// 2. 使其他 CPU 缓存该地址失效(MESI 协议)本质:volatile 变量的读写都会绕过 CPU 缓存,直接操作主内存。
Q3:volatile 和 synchronized 在可见性上有什么区别?
答:
// volatile:只保证可见性
volatile int count = 0;
// synchronized:可见性 + 原子性
synchronized void increment() { count++; }核心:synchronized 通过 Monitor 的 monitorenter/exit 也保证可见性(释放锁时刷回主内存)。
Q4:哪些操作会保证可见性?
答:
volatile 读写
synchronized 释放/获取锁
final 字段初始化(安全发布)
线程启动/终止:
start()前的操作对子线程可见中断:
interrupt()前的操作对被中断线程可见并发工具: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;重排类型:
编译器重排:编译期优化
CPU 重排:乱序执行
内存重排:缓存和写缓冲导致
原则:不改变单线程执行结果(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 如何禁止指令重排?
答: 通过插入内存屏障:
volatile 写:
StoreStore 屏障
volatile 写
StoreLoad 屏障volatile 读:
LoadLoad 屏障
volatile 读
LoadStore 屏障// JMM 规定的 volatile 重排规则
// 1. volatile 写之前的操作,不能重排到写之后
// 2. volatile 读之后的操作,不能重排到读之前
// 3. volatile 写不能与 volatile 读重排Q11:什么是 happens-before 原则?
答: JMM 定义的偏序关系,保证可见性和有序性:
// 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 限制重排。
注意:as-if-serial 不保证多线程正确性。
三、不保证原子性
Q15:什么是原子性?
答: 原子性指一个操作不可分割,要么全执行,要么全不执行:
int x = 1; // 原子(基本类型赋值)
x++; // 非原子(读-改-写三步)
long y = 1L; // 32位 JVM 上非原子x++ 实际步骤:
读取 x
计算 x+1
写回 x
多线程下可能丢失更新。
Q16:为什么 volatile 不保证原子性?
答: volatile 只保证可见性和有序性,不保证复合操作的原子性:
volatile int count = 0;
// 10 个线程各执行 1000 次
count++; // 非原子!问题:
线程 A 读 count=0
线程 B 读 count=0
A 写 count=1
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:如何保证原子性?
答: 三种方式:
synchronized:
synchronized void increment() { count++; }Lock:
lock.lock();
try { count++; } finally { lock.unlock(); }原子类:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 保证原子推荐:低竞争用原子类,高竞争用 LongAdder,复杂逻辑用锁。
Q19:volatile 适用哪些场景?
答:
状态标志:
volatile boolean running = true;
while (running) { /* 工作 */ }双重检查锁:
private static volatile Singleton instance;一次性安全发布:
volatile Config config;
config = loadConfig(); // 发布独立观察:
volatile int temperature; // 定期更新,其他线程读取不适用:
复合操作(count++)
依赖当前值的操作
多变量一致性
Q20:volatile 和 AtomicInteger 如何选择?
答:
// 标志位:volatile
volatile boolean shutdown = false;
// 计数:AtomicInteger
AtomicInteger count = new AtomicInteger(0);Q21: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 指令,禁止特定重排:
volatile 写的屏障:
StoreStore
volatile 写
StoreLoadvolatile 读的屏障:
LoadLoad
volatile 读
LoadStorex86 架构:只有 StoreLoad 需要显式指令(mfence),其他是空操作。
Q25:JMM 是什么?
答: JMM(Java Memory Model)是多线程内存模型规范:
定义线程和主内存的抽象关系
规定可见性、有序性、原子性规则
屏蔽不同硬件的差异
核心内容:
主内存 + 工作内存抽象
happens-before 原则
内存屏障指令
volatile、synchronized、final 语义
线程 A 线程 B
工作内存A 工作内存B
| |
+--- 主内存 ---+Q26:volatile 的底层实现是什么?
答: 通过 lock 前缀指令:
volatile int x = 1;
// 编译后:
// mov [x], 1
// lock addl $0, (%rsp) ; 内存屏障lock 指令的作用:
将当前缓存行写回内存
使其他 CPU 的缓存行失效(MESI)
禁止指令重排
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 在并发工具中的应用?
答:
AQS 的 state:
private volatile int state;ConcurrentHashMap 的节点:
volatile V val;
volatile Node<K,V> next;ThreadPoolExecutor 的 ctl:
private final AtomicInteger ctl = ...;
// 内部用 volatile + CASCopyOnWriteArrayList 的 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 的最佳实践是什么?
答:
仅用于状态标志:
volatile boolean running = true;DCL 单例必须加:
private static volatile Singleton instance;不要用于复合操作:
volatile int count = 0;
count++; // 错误!配合 CAS 使用:
AtomicInteger count = new AtomicInteger(0);优先用现成工具:
AtomicBoolean、LongAdder、ConcurrentHashMap四、综合对比
Q31: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 语义:
写语义:
写 volatile 时,把工作内存刷新到主内存
等价于释放锁
读语义:
读 volatile 时,从主内存重新加载
等价于获取锁
总结:
volatile 写 ≈ 解锁
volatile 读 ≈ 加锁这是理解 volatile 的关键。
评论