Java 中 volatile 关键字到底保证了什么?
Java 的 volatile 只保证可见性和有序性(禁止重排序),不保证原子性;i++ 等复合操作需用 AtomicInteger 或 synchronized,适合状态标志、DCL 等一写多读场景,不适合计数器。
**volatile 只保证两件事:可见性和有序性(禁止指令重排序),它不保证原子性。**换句话说,被 volatile 修饰的变量,一个线程改了别的线程能立刻看到,且编译器与 CPU 不会把它前后的指令乱序搬动;但 volatile int count; count++ 依然线程不安全。
volatile 保证了什么?可见性 + 有序性,就这两条
结论:volatile 的语义只有两个——可见性和禁止重排序,别的都别指望。
可见性指:线程 A 对 volatile 变量的写,会立刻刷回主内存;线程 B 之后读同一个 volatile 变量时,会从主内存重新读取,而不是用自己工作内存里的旧副本。JMM(Java 内存模型)里对应的规则是:对一个 volatile 变量的写 happens-before 后续对同一个变量的读。
有序性指:volatile 写之前的普通读写不会排到写之后,volatile 读之后的普通读写不会排到读之前。所以 volatile 变量相当于一道「栅栏」,它本身不搬,但挡住别人乱跑。
volatile 为什么不保证原子性?i++ 这个反例最典型
结论:i++ 是「读—改—写」三条指令,volatile 只能保证每一步的可见性,不能保证这三步合起来不被插队。
两个线程同时执行 volatile int i = 0; i++;,可能出现都读到 0、各自加 1 写回 1,最终结果仍是 1。要修就换 AtomicInteger(底层用 CAS,compare-and-swap,比较并交换),或者用 synchronized 包住整段逻辑。同理,volatile int[] arr 只保证数组引用这个「指针」的可见性,不保证 arr[0] 这种元素读写的可见性,要用 AtomicIntegerArray。
volatile 的可见性和有序性是怎么实现的?
结论:在 JMM 层面靠 happens-before 规则,在 HotSpot 实现层面靠内存屏障(Memory Barrier)。
具体来说,编译器和 JVM 会在 volatile 写之前插入 StoreStore 屏障、写之后插入 StoreLoad 屏障;在 volatile 读之后插入 LoadLoad 和 LoadStore 屏障。在 x86 平台上,StoreLoad 这个最贵的屏障通常用带 lock 前缀的指令实现,例如 lock addl $0x0,(%rsp)。字节码层面 volatile 字段带有 ACC_VOLATILE 访问标志(JDK 5 起,也就是 JSR-133 之后语义才完整)。
什么时候该用 volatile?三个场景够用了
结论:volatile 适合「一个线程写、多个线程读」的状态标志和一次性安全发布,不适合做计数器。
- 状态开关:
private volatile boolean running = true;一个线程running = false,其他循环线程立刻能退出。 - 双重检查锁定(DCL)单例:
private static volatile Singleton instance;。因为new Singleton()分三步——分配内存、初始化对象、把引用赋给变量,第 2、3 步可能重排序。不加 volatile,别的线程可能拿到一个「引用非空但字段还是默认值」的半成品对象。这个问题在 JDK 5 之前即使加了 volatile 也存在,JDK 5 修复。 - 安全发布:把一个对象引用写成 volatile,读线程看到这个引用时,就能看到构造该对象之前的所有写入。
volatile 和 synchronized 有什么区别?
结论:synchronized 同时保证原子性、可见性和有序性,volatile 只保证后两个,而且不阻塞线程。
volatile 没有加锁、不会引起线程上下文切换,所以开销远小于 synchronized,但也因此不能保护复合操作。判断标准很简单:**如果这段逻辑需要「检查再执行」或者多个变量之间保持一致性,就用 synchronized 或锁;如果只是共享一个标志位或引用,volatile 就够了。**
总结一句:volatile = 可见性 + 禁止重排序,原子性不在它的职责范围内;i++、check-then-act、多变量联动这三类场景,请换 AtomicInteger 或锁。
转载请注明出处,版权归原作者所有。