资深 Java 面试到 Agent 开发工程师 · 第 1 集
先看问题
class Worker {
private boolean stop = false;
void run() {
while (!stop) {
doWork();
}
}
void shutdown() {
stop = true;
}
}
两个线程操作同一个 Worker 对象:线程甲执行 run(),线程乙执行 shutdown()。
线程乙把 stop 改成 true 后,按直觉,线程甲下一次检查 stop 就应该退出循环。但 Java 只凭这段代码,无法保证线程甲会读到这次修改。它可能继续读到旧值 false,所以循环可能一直不退出。
这里实际发生了两个动作:
线程乙:stop = true; // 写入
线程甲:读取 stop // 读取
代码里虽然有写入和读取,但两个动作之间没有锁、volatile 或其他同步操作。线程乙写完,不等于线程甲已经看见。
JMM 到底规定什么
Java 内存模型(JMM)不是一块所有线程共用的物理内存,也不是要求每次写入都立刻刷到内存条。它是一组规则,规定线程怎样共享变量,以及什么条件下一个线程的读取必须看到另一个线程的写入。
它要回答两个问题:
同一线程内,程序顺序让 stop = true happens-before 后面的动作。编译器和处理器可以调整实际执行顺序,但必须满足 Java 内存模型的要求,包括同步带来的跨线程约束;这个线程内的关系也不会自动连接到线程甲。
这段代码没有同步关系,所以 Java 不承诺线程甲什么时候能读到新值。Java 也没有规定线程甲每次循环都要重新看到线程乙的写入,继续使用先前读到的 false 仍然符合这段代码的规则。
happens-before 是什么
Java 用 happens-before 表示一条可以依赖的先后关系,由它规定哪些可见性和顺序有保证。
如果 A happens-before B:
- 按 Java 定义的顺序,A 先于 B;
- 如果 A 写入共享变量,B 读取同一个变量,且某次旧写入 happens-before A,那么 B 不能读取那次旧写入。若还有其他与读取未建立先后关系的并发写入,B 也不一定读到 A 写入的值。
放回停止标志的例子:
线程乙写入 stop = true
│
│ happens-before
▼
线程甲读取 stop = true
这条关系不会自动出现。常见规则包括:
- 同一把锁的解锁 happens-before 后续对这把锁的加锁;
- 一个字段的 volatile 写入 happens-before 后续对这个字段的读取;
- 调用 start() 的动作 happens-before 新线程中的动作;
- 线程结束 happens-before 另一个线程成功返回 join()。
没有这条关系时,不能把两个线程里的源码顺序拼成一条执行顺序。

用 volatile 修复停止标志
class Worker {
private volatile boolean stop = false;
void run() {
while (!stop) {
doWork();
}
}
void shutdown() {
stop = true;
}
}
stop 声明为 volatile 后,线程乙写入 stop = true,线程甲随后读取同一个字段时,这次写入会通过 volatile 规则对读取可见。线程甲下一次读到 true 后,循环就能退出。
volatile 只解决这个字段的可见性,并为相关读写提供有序性。它不会让其他字段的单独读写自动变成线程安全,也不会把整段业务代码变成原子操作。如果 doWork() 正在阻塞,volatile 也不会替它中断这个调用。
为什么要先写数据,再写 ready
volatile 还可以用来发布已经准备好的数据。
下面假设两个线程操作的是同一个 Config 对象,而且 config 引用本身已经通过线程启动、锁或其他同步方式安全地共享给读取线程。
class Config {
int data;
volatile boolean ready;
}
写入线程:
config.data = 42;
config.ready = true;
读取线程:
if (config.ready) {
System.out.println(config.data);
}
ready 表示“data 已经写完”。它必须最后写入,因为这次 volatile 写会把同一线程中它之前的写入传递给读取线程。
data = 42
│ 同一个线程里的程序顺序
▼
ready = true
│ ready 的 volatile 写入 happens-before 读取线程对 ready 的读取
▼
读取 ready == true
│ 同一个线程里的程序顺序
▼
读取 data
因此,读取线程看到 ready == true 时,也能看到之前写入的 data == 42。
如果先把 ready 改成 true,数据还没有准备好,读取线程就可能看到 true,却读到旧值或默认值。
volatile 不能解决 count++
private volatile int count;
count++;
count++ 包含三步:
读取 count
把结果加一
写回 count
两个线程可能同时读到 0,然后都把 1 写回去。在这个交错下,最终结果是 1,不是 2。
volatile 能让单次读写具备可见性,但不能把“读—改—写”三步合成一个不可分割的动作。可见性和原子性是两个问题。
常见面试题怎么答?
1. JMM 和 happens-before 解决什么问题?
JMM 规定线程怎样共享变量,以及哪些可见性和顺序有保证。同一线程的程序顺序、同一把锁的释放与后续获取、volatile 写入与后续读取等可以建立 happens-before 关系,让另一个线程依赖之前完成的写入。
2. volatile 为什么不能保证 count++ 正确?
count++ 包含读取、加一、写回。volatile 提供单次读写的内存语义,两个线程仍可能读到相同的旧值并写回同一个结果。要保护整个更新,应使用原子类或同一把锁。
3. 为什么要先写 data,再写 volatile ready?
写线程先准备 data,后发布 ready。读线程读到 ready 为 true 后,才能沿 happens-before 关系看到前面写好的 data。先发布状态,就可能允许读取方在数据尚未准备好时读取。
读完自测
1. 为什么线程乙执行了 stop = true,线程甲仍然可能继续循环?
参考要点:两个线程之间缺少同步关系,普通字段的写入没有对另一个线程的读取建立可见性保证。可以用同一个 volatile 字段通信;若线程正阻塞在 doWork(),还要处理实际调用的超时或取消。
2. volatile 写入和后续读取之间建立了什么关系?
参考要点:对同一字段的 volatile 写入 happens-before 后续读取。结合写线程、读线程各自的程序顺序,读取方还能看到该次写入之前已经准备好的数据。
3. 为什么 volatile int count 仍然不能保证 count++ 的结果正确?
参考要点:count++ 包含读取、加一、写回。两个线程可能都读到同一个旧值,再写回相同的新值;可见性不会把这三步合成原子操作。
规范依据:Java Language Specification 17:Threads and Locks(Java 8)
网硕互联帮助中心



评论前必须登录!
注册