时间:2025-11-07 09:19
人气:
作者:admin
“你以为程序按顺序执行,但CPU和JVM说:不,我们有自己的想法。”
一起来解剖一段看似简单、实则暗藏玄机的Java代码。它只有20行,却浓缩了多线程编程中最经典、最易被忽视的陷阱——可见性(Visibility)问题与指令重排序(Reordering)。
它来自《Java并发编程实战》(JCIP)的经典示例,也是无数面试题的源头。
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield(); // 礼貌地让出CPU
}
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start(); // 启动读线程
number = 42; // 先赋值
ready = true; // 再“通知”
}
}
ReaderThread,它不断检查 ready 是否为 true;number 设为 42,再将 ready 设为 true,表示“数据已就绪”;ready == true 后,打印 number,理应输出 42。多次运行,你可能会看到:
42 ✅(幸运时刻)0 ⚠️(高频出现!)Ctrl+C)???????? 这段代码没有
synchronized,没有锁,没有异常——它“语法正确”,却“语义错误”。问题出在哪?
现代CPU为提升性能,每个线程都有自己的工作内存(高速缓存)。对共享变量的读写,可能只发生在本地缓存,不立即同步到主内存。
ready = true,但这个值可能还“躺”在它的缓存里;ReaderThread 的缓存里 ready 仍是 false → 无限循环;ready == true,它的缓存里 number 可能还是初始值 0 → 打印 0。⚠️
Thread.yield()只是建议线程让出CPU时间片,并不触发缓存刷新!它无法解决可见性问题。
为优化性能,JVM 和 CPU 在不改变单线程语义的前提下,允许重排指令顺序:
// 你写的:
number = 42;
ready = true;
// 实际执行的,可能是:
ready = true; // 先执行!
number = 42; // 后执行!
对主线程自己来说,结果一样;但对 ReaderThread 而言,它可能在 ready 变成 true 的瞬间跳出循环,此时 number 还没被写入——于是读到 0。
???? 重排序是合法的,只要你没用同步机制“约束”它。
Java 内存模型用 happens-before 规则定义操作间的可见性顺序。若操作 A happens-before 操作 B,则 A 的结果对 B 一定可见。
而上述代码中:
number = 42 与 ready = true 之间 没有 happens-before 关系;ready 与读线程读 ready 之间 也没有 happens-before 关系。结果就是:一切皆有可能(0、42、死循环)——典型的竞态条件(Race Condition)。
要让 ReaderThread 在看到 ready == true 时 必然 看到 number == 42,我们必须建立明确的 happens-before 边界。
volatile —— 最简洁优雅(推荐!)private static volatile boolean ready; // ← 只需加在这里!
private static int number; // number 可以不加 volatile
Java 内存模型规定:
“对一个 volatile 变量的写操作 happens-before 后续对这个 volatile 变量的读操作。”
这意味着:
ready = true(volatile 写);ReaderThread 执行 if (!ready)(volatile 读)并看到 true;number = 42 →(程序顺序)→ ready = true(volatile写)ready 读取为 truenumber = 42 happens-before 读取 number!✅ number 即便不是 volatile,也能被正确看到为 42!
???? 这就是
volatile的“内存可见性传递性”:一个 volatile 写,能“捎带”它之前所有普通写操作的可见性 。
synchronized —— 重量级但通用private static final Object lock = new Object();
// ReaderThread 中:
while (!ready) {
synchronized (lock) { } // 空同步块,只为建立同步边
Thread.yield();
}
// main 中:
synchronized (lock) {
number = 42;
ready = true;
}
synchronized 天然提供:
AtomicBoolean / AtomicIntegerprivate static final AtomicBoolean ready = new AtomicBoolean(false);
private static final AtomicInteger number = new AtomicInteger(0);
// main:
number.set(42);
ready.set(true);
// ReaderThread:
while (!ready.get()) {
Thread.yield();
}
System.out.println(number.get());
AtomicXxx 的 get()/set() 默认具有 volatile 语义(除 lazySet),同样满足 happens-before 。
你可以在本地反复运行原版代码:
for i in {1..10}; do java NoVisibility; done
# 很可能混杂着 0 和 42,甚至卡住
再运行修复版(加 volatile):
for i in {1..10}; do java FixedNoVisibility; done
# 稳定输出 42!
???? 提示:在服务器模式(
-serverJVM)或某些CPU架构(如ARM)上,问题更容易复现。
| 误区 | 真相 |
|---|---|
| “变量赋值是原子的,所以没问题” | 原子性 ≠ 可见性。boolean/int 赋值是原子的,但其他线程看不到! |
“Thread.yield() 能让线程‘同步’” |
yield() 是线程调度提示,无内存语义,不能替代同步。 |
| “代码顺序 = 执行顺序” | 编译器、CPU、JIT 都会重排序——除非你用 volatile/synchronized 禁止。 |
| “单核CPU不会有这问题” | 单核也可能缓存不一致!且现代基本都是多核。 |
volatile 不只是“防重排序”,更是建立 happens-before 的轻量级工具;volatile flag,可带动一批普通变量的可见性——这是高效并发设计的基石;
Hutool 的 `TimedCache` 到期会自动清理吗?