深入理解 Java volatile:从可见性到内存屏障 深入理解 Java volatile从可见性到内存屏障在 Java 并发编程中volatile是一个“看起来简单但真正理解并不容易”的关键字。很多人知道volatile 能保证可见性volatile 不能保证原子性但为什么能保证可见性为什么不能保证原子性volatile 为什么能禁止指令重排序DCL 单例为什么一定要加 volatilevolatile 和 synchronized 到底有什么区别本文尝试从 JVM 内存模型到底层 CPU 缓存一致性系统地讲清楚 volatile。一、volatile 是什么volatile是 Java 提供的一个轻量级并发关键字。它主要有两个作用保证变量可见性Visibility禁止指令重排序Ordering但volatile 不保证原子性Atomicity二、为什么会有线程可见性问题先看一个经典例子publicclassTest{staticbooleanflagtrue;publicstaticvoidmain(String[]args)throwsException{newThread(()-{while(flag){}System.out.println(线程结束);}).start();Thread.sleep(1000);flagfalse;}}很多人以为1 秒后 flagfalse while(flag) 结束 程序退出但实际上程序可能永远不会结束。为什么三、Java 内存模型JMMJava 并发并不是线程直接操作主内存。而是主内存 ↑ ↓ 线程工作内存CPU缓存线程会把变量从主内存复制到自己的工作内存中。所以线程A修改了变量 线程B不一定立即看到在上面的例子中主线程把flag改成了false但子线程可能一直读取自己的缓存导致死循环四、volatile 如何保证可见性给变量加上 volatilestaticvolatilebooleanflagtrue;这时 JVM 会保证volatile 写修改后立即刷新到主内存volatile 读每次都从主内存读取最新值于是主线程修改 flagfalse ↓ 刷新到主内存 ↓ 子线程重新读取 ↓ while结束五、volatile 为什么不能保证原子性这是最经典的问题。例如volatileintcount0;count;很多人误以为count 是原子操作其实不是。它实际包含三步1. 读取 count 2. count 1 3. 写回 count多个线程同时执行线程A读取 count5 线程B读取 count5 线程A写回6 线程B写回6结果丢失更新六、代码验证 volatile 不保证原子性publicclassTest{volatilestaticintcount0;publicstaticvoidmain(String[]args)throwsException{for(inti0;i10;i){newThread(()-{for(intj0;j1000;j){count;}}).start();}Thread.sleep(2000);System.out.println(count);}}理论结果10000实际结果8234 9365 9971 ...原因volatile 只能保证“看到最新值”不能保证“复合操作不被打断”。七、volatile 的第二个核心作用禁止指令重排序这是 volatile 最重要、也是最容易被忽略的作用。八、什么是指令重排序为了提高性能编译器会优化代码顺序CPU 会乱序执行JVM JIT 会优化执行路径例如inta1;intb2;可能实际执行顺序b 2 a 1只要单线程结果不变JVM 就允许优化。这叫指令重排序Instruction Reordering九、指令重排序带来的问题看一个经典 DCL 单例publicclassSingleton{privatestaticSingletoninstance;publicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton.class){if(instancenull){instancenewSingleton();}}}returninstance;}}看起来没问题。但实际上instancenewSingleton();不是原子操作。它可能被拆成1. 分配内存 2. 引用指向内存 3. 调用构造函数但 JVM 可能重排序1. 分配内存 2. 引用指向内存 3. 初始化对象这时instance ! null但对象还没初始化完成。另一个线程获取后得到“半初始化对象”会出现严重问题。十、为什么 DCL 必须加 volatile正确写法publicclassSingleton{privatestaticvolatileSingletoninstance;publicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton.class){if(instancenull){instancenewSingleton();}}}returninstance;}}volatile 会禁止 instance new Singleton() 重排序从而保证对象一定初始化完成后 才对其他线程可见十一、volatile 底层原理volatile 本质依赖JVM 内存屏障Memory BarrierCPU 缓存一致性协议MESI十二、什么是内存屏障JVM 在 volatile 前后插入特殊 CPU 指令Load Barrier Store Barrier作用保证可见性强制刷新缓存。禁止重排序限制 CPU/JIT 优化。十三、volatile 的 Happens-Before 规则Java 内存模型规定对 volatile 变量的写Happens-Before 后续对该变量的读。即线程A: volatileWrite 一定先于 线程B: volatileRead这是 volatile 可见性的理论基础。十四、volatile 和 synchronized 区别特性volatilesynchronized可见性支持支持有序性支持支持原子性不支持支持是否阻塞线程否是性能开销小较大十五、volatile 适用场景volatile 非常适合1. 状态标记volatilebooleanshutdown;2. 配置刷新volatileConfigconfig;3. DCL 单例privatestaticvolatileSingletoninstance;4. 一个线程写多线程读volatile 最适合写少读多场景。十六、volatile 不适合什么场景不适合计数器count复合操作if(flag){doSomething();}CAS竞争激烈场景此时应该synchronizedLockAtomicIntegerLongAdder十七、volatile vs AtomicInteger很多人问既然 volatile 不能保证原子性 为什么 AtomicInteger 可以因为AtomicInteger volatile CAS其中volatile 保证可见性CAS 保证原子更新十八、面试高频问题总结volatile 能保证线程安全吗不能。它只能保证可见性有序性不能保证原子性volatile 和 synchronized 区别volatile 更轻量。synchronized 更强。DCL 为什么必须加 volatile防止指令重排序导致返回未初始化对象volatile 底层原理内存屏障CPU缓存一致性协议Happens-Before十九、一句话总结 volatilevolatile 的本质让一个线程写入的数据 对其他线程立即可见它解决的是“看得见”而不是“抢得到”所以volatile 保证可见性 synchronized 保证原子性这是理解 Java 并发最核心的一组概念。