volatile关键字解决可见性问题解决指令重排序不保证原子性volatile关键字底层原理——内存屏障面试题volatile 关键字的作用是什么volatile 和 synchronized 的区别volatile 能替代锁吗为什么volatile 底层是怎么实现的什么是内存屏障有哪些类型DCL 单例为什么要加 volatilevolatile 变量写操作后其他线程为什么能看到这段代码为什么会有问题countJava 内存模型JMM中的 8 个原子操作和 volatile 的关系volatile 和 final 在内存语义上有什么区别除了 volatile还有什么方式能保证可见性解决可见性问题先看一段代码publicclassVisibilityDemo{privatestaticbooleanflagfalse;publicstaticvoidmain(String[]args)throwsInterruptedException{//读线程newThread(()-{System.out.println(线程A等待flag变为true);while(!flag){// 忙等待}System.out.println(线程A检测到flag为true退出);}).start();Thread.sleep(1000);//写线程newThread(()-{flagtrue;System.out.println(线程B已将flag设为true);}).start();}}结果分析线程B将 flag 改为 true 后线程A应该立即跳出循环。但实际运行结果往往是线程A陷入了死循环永远无法退出。为什么会这样这涉及到并发编程中第一个核心问题——可见性。在 Java 内存模型JMM中每个线程都有自己的工作内存CPU 缓存共享变量存储在主内存中。线程B修改了 flag只是更新了自己工作内存中的副本还没来得及同步到主内存而线程A读取 flag 时也只会从自己的工作内存中读取。两个线程各自维护着一份副本自然看不到对方的变化。volatile 的第一个作用就是解决这个问题将 flag 声明为volatile boolean flag false;后任何线程对它的修改都会立即刷新到主内存任何线程读取它时也会直接从主内存获取最新值从而保证可见性。解决指令重排序解决了可见性问题还有第二个陷阱。来看单例模式中的经典实现——双重检查锁定Double-Checked LockingpublicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){// 第一次检查synchronized(Singleton.class){if(instancenull){// 第二次检查instancenewSingleton();}}}returninstance;}}这段代码看似完美实际上存在隐患。问题出在instance new Singleton()这一行它不是原子操作而是分为多个步骤分配内存空间初始化对象执行构造函数将 instance 引用指向分配的内存空间为了提高执行效率编译器和处理器可能会对指令进行重排序将步骤3提前到步骤2之前。此时如果另一个线程恰好在 instance 不为 null 但对象还未初始化完成时调用getInstance()就会拿到一个半成品对象可能导致程序崩溃。volatile 的第二个作用就是禁止指令重排序将 instance 声明为private static volatile Singleton instance;后JVM 会在 volatile 写操作前后插入内存屏障确保instance new Singleton()的执行顺序不会被重排从而避免对象逸出问题。不保证原子性volatile 解决了可见性和有序性但有一个重要的限制需要牢记它不保证原子性。publicclassAtomicityDemo{privatestaticvolatileintcount0;publicstaticvoidmain(String[]args)throwsInterruptedException{CountDownLatchlatchnewCountDownLatch(10);for(inti0;i10;i){newThread(()-{for(intj0;j1000;j){count;// 复合操作非原子性}latch.countDown();}).start();}latch.await();System.out.println(期望值10000实际值count);}}多次运行这段代码你会发现 count 的值总是小于 10000。原因在于count虽然看起来是一行代码实际上包含三个步骤读取值 → 加1 → 写回。volatile 只能保证每次读取时拿到最新的值但无法防止多个线程交错执行这些步骤导致的丢失更新问题。如果需要原子性操作应该使用AtomicInteger或synchronized。volatile关键字底层原理——内存屏障volatile 的可见性和有序性底层都是通过内存屏障Memory Barrier实现的。内存屏障是一种 CPU 指令分为四种类型屏障类型作用LoadLoad读-读屏障后面的读不能重排到前面的读之前StoreStore写-写屏障后面的写不能重排到前面的写之前LoadStore读-写屏障后面的写不能重排到前面的读之前StoreLoad写-读屏障后面的读不能重排到前面的写之前全能型开销最大 同时刷新写缓冲区对于 volatile 变量的操作JVM 会按以下策略插入内存屏障volatile 写之前插入 StoreStore 屏障确保之前的普通写操作已完成volatile 写之后插入 StoreLoad 屏障强制将新值刷新到主内存volatile 读之后插入 LoadLoad 和 LoadStore 屏障禁止后续读写被重排到前面这些屏障在底层会被优化。x86 本身就是强内存模型它保证了LoadLoad、LoadStore、StoreStore顺序所以其实在 x86 上volatile的底层实现通常只需要一个lock addl对应StoreLoad就足够了。面试题volatile 关键字的作用是什么保证可见性一个线程修改 volatile 变量后其他线程能立即看到最新值禁止指令重排序通过内存屏障确保有序性不保证原子性复合操作如count仍有并发风险volatile 和 synchronized 的区别维度volatilesynchronized可见性✅✅原子性❌✅线程阻塞不会会性能开销低高有锁竞争使用位置变量修饰方法/代码块volatile 能替代锁吗为什么不能因为volatile只保证可见性和有序性对于复合操作读-改-写无能为力必须用锁或 CAS举例count用 volatile 依然会丢更新volatile 底层是怎么实现的JVM 层面通过内存屏障Memory Barrier实现对 volatile 变量的写操作会插入StoreStore和StoreLoad屏障对 volatile 变量的读操作会插入LoadLoad和LoadStore屏障硬件层面x86通过lock前缀指令实现如lock addllock会锁定总线或缓存行强制将修改刷新到主存并使其他 CPU 缓存行失效什么是内存屏障有哪些类型屏障类型作用LoadLoad读-读屏障后面的读不能重排到前面的读之前StoreStore写-写屏障后面的写不能重排到前面的写之前LoadStore读-写屏障后面的写不能重排到前面的读之前StoreLoad写-读屏障后面的读不能重排到前面的写之前全能型开销最大DCL 单例为什么要加 volatileinstance new Singleton()不是原子操作分三步分配内存初始化对象执行构造方法将引用指向内存步骤 2 和 3 可能被重排导致对象半初始化时引用就不为 null其他线程拿到这个半成品对象可能出问题字段为默认值volatile 禁止了对象创建过程的重排序volatile 变量写操作后其他线程为什么能看到JMM 规范volatile 写会将当前工作内存的值强制刷新到主内存同时会使其他 CPU 中该变量的缓存行失效MESI 协议的 Invalidate 机制其他线程再读时必须从主内存重新加载这是 volatile 可见性的完整闭环JMM这段代码为什么会有问题countvolatileintcount0;// 10个线程各加1000次count;count是复合操作读 → 加1 → 写volatile 只保证读和写的可见性但两次操作之间可能被其他线程打断最终结果会小于 10000解决方案使用AtomicInteger或synchronizedJava 内存模型JMM中的 8 个原子操作和 volatile 的关系8 个操作lock、unlock、read、load、use、assign、store、writevolatile 规定对变量的 read-load-use 必须连续assign-store-write 也必须连续本质上是约束了这些原子操作的执行顺序不允许中间插入其他操作volatile 和 final 在内存语义上有什么区别final 用于保证初始化安全性构造函数执行完后final 字段对任意线程可见volatile 保证的是每次读写的可见性一个对象的所有 final 字段在构造函数结束时会有一个冻结操作StoreStore 屏障final 的优势初始化后不需要同步开销适合不可变对象final有点像string但是string本质不是final实现除了 volatile还有什么方式能保证可见性synchronized锁释放前会将工作内存刷新到主存Lock与 synchronized 类似AtomicInteger等原子类底层用 volatile 修饰 value 字段final初始化安全性保证构造结束后的可见性Thread.join()/Thread.start()JMM 保证的 happens-before 规则