
Java 中对 int 等基本类型(除 long/double 外)的读写天然具有原子性,但这仅确保操作“不会撕裂”,绝不意味着其他线程能立即看到最新值;线程间变量修改的可见性必须依赖 volatile、锁或同步机制来保障。
java 中对 `int` 等基本类型(除 `long`/`double` 外)的读写天然具有原子性,但这仅确保操作“不会撕裂”,**绝不意味着其他线程能立即看到最新值**;线程间变量修改的可见性必须依赖 `volatile`、锁或同步机制来保障。
在并发编程中,“原子性”(atomicity)和“可见性”(visibility)是两个正交但常被混淆的核心概念。JLS(Java 语言规范)明确指出:对 byte、short、char、int、boolean 和引用类型的单次读/写操作是原子的;而 long 和 double 的非 volatile 读写则可能因 JVM 实现(如 32 位 CPU 分两次写入 64 位值)导致“字撕裂”(torn write)。例如:
public class TornLongExample {
private long value = 0x00000000_12345678L;
// Thread A: 写入新值
public void update() {
value = 0x12345678_00000000L; // 可能分两步写入
}
// Thread B: 读取值
public long read() {
return value; // 可能读到 0x12345678_12345678L —— 从未写入过的“脏组合”
}
}⚠️ 但请注意:即使 int 的读写是原子的,它仍完全不提供跨线程可见性保证。以下代码存在典型可见性问题:
public class VisibilityProblem {
private int flag = 0;
public void writer() {
flag = 1; // 原子写,但可能仅写入本线程本地缓存(CPU cache / JIT 优化)
}
public void reader() {
while (flag == 0) {
// 死循环!即使其他线程已将 flag 设为 1,
// 当前线程可能永远读取寄存器/缓存中的旧值 0
}
System.out.println("Flag changed!");
}
}JIT 编译器可能将 flag 提升为循环内不变量(loop invariant hoisting),甚至将其优化为无限 while(true);CPU 缓存一致性协议也无法自动同步非 volatile 变量的更新。
✅ 正确做法是使用 volatile 显式声明需跨线程可见的变量:
立即学习“Java免费学习笔记(深入)”;
public class FixedVisibility {
private volatile int flag = 0; // ✅ 同时提供:原子性(int 本身) + 可见性 + 禁止重排序
public void writer() {
flag = 1; // 写入后,所有线程立即可见
}
public void reader() {
while (flag == 0) { // 每次循环都从主内存(或最新缓存行)重新读取
Thread.onSpinWait(); // 推荐:提示 CPU 优化自旋等待
}
System.out.println("Flag changed!");
}
}? 关键总结:
- 原子性 ≠ 可见性:原子性防止操作中途被中断或数据错乱;可见性确保一个线程对变量的修改对其他线程及时生效。
- volatile 对 int 等类型的作用不是增强原子性(它本来就是原子的),而是建立 Happens-Before 关系,强制刷新缓存、禁止指令重排,并确保每次读取都获取最新值。
- long/double 使用 volatile 是一箭双雕:既修复潜在的非原子性(字撕裂),又获得可见性与有序性。
- 若需复合操作(如 i++),即使变量是 volatile int 也不够——此时必须用 AtomicInteger 或 synchronized,因为 i++ 本身包含读-改-写三步,非原子。
因此,在多线程环境下,切勿因“int 写是原子的”而省略 volatile;是否需要它,取决于你是否要求修改对其他线程可见——而这正是并发正确性的基石之一。









