前言在 Java 多线程面试中volatile 是一个非常高频的关键字。它看起来简单只是在变量前面加一个修饰符但背后涉及 Java 内存模型、线程可见性、指令重排序、原子性等并发基础。面试官常见问法有volatile 有什么作用volatile 能保证线程安全吗volatile 和 synchronized 有什么区别为什么双重检查锁单例要加 volatilevolatile 能不能保证 i 的原子性这篇文章从面试角度系统总结一下 volatile 的相关用法。一、volatile 是什么volatile 是 Java 提供的一个轻量级同步机制可以用来修饰变量。基本写法如下privatevolatilebooleanflagtrue;它主要有两个核心作用保证线程之间的可见性禁止指令重排序但是需要特别注意volatile 不能保证复合操作的原子性这句话是面试里的重点。二、为什么需要 volatile在多线程环境下每个线程可能会把共享变量从主内存复制到自己的工作内存中操作。如果一个线程修改了共享变量另一个线程不一定能立刻看到最新值。可以简单理解为主内存保存共享变量线程工作内存线程自己使用的变量副本如果没有同步机制可能出现下面的问题publicclassVolatileDemo{privatestaticbooleanrunningtrue;publicstaticvoidmain(String[]args)throwsInterruptedException{newThread(()-{while(running){// 执行任务}System.out.println(线程停止);}).start();Thread.sleep(1000);runningfalse;}}理论上主线程把 running 改成 false 后子线程应该停止。但如果 running 没有使用 volatile 修饰子线程可能一直读取自己工作内存中的旧值导致循环无法结束。修改后publicclassVolatileDemo{privatestaticvolatilebooleanrunningtrue;publicstaticvoidmain(String[]args)throwsInterruptedException{newThread(()-{while(running){// 执行任务}System.out.println(线程停止);}).start();Thread.sleep(1000);runningfalse;}}加上 volatile 后一个线程修改变量其他线程可以更及时地看到最新值。三、volatile 的第一个作用保证可见性可见性指的是一个线程修改了共享变量的值其他线程能够立刻看到这个修改。使用 volatile 修饰变量后对 volatile 变量的写操作会立即刷新到主内存对 volatile 变量的读操作会从主内存读取最新值典型场景是线程停止标记publicclassTaskimplementsRunnable{privatevolatilebooleanstoppedfalse;publicvoidstop(){stoppedtrue;}Overridepublicvoidrun(){while(!stopped){System.out.println(任务执行中);}System.out.println(任务已停止);}}这种场景下volatile 非常合适因为 stopped 只是一个状态标记读写操作都很简单。四、volatile 的第二个作用禁止指令重排序为了提高执行效率编译器和 CPU 可能会对指令进行重排序。在单线程环境下重排序不会影响最终结果但在多线程环境下重排序可能导致线程安全问题。最经典的例子就是双重检查锁单例模式。不加 volatile 的问题 - 懒汉式情况publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton.class){if(instancenull){instancenewSingleton();}}}returninstance;}}看起来这段代码已经使用了 synchronized但仍然可能有问题。因为创建对象并不是一个简单操作大致可以拆成三步2. 分配对象内存3. 初始化对象4. 把对象引用赋值给 instance在某些情况下步骤 2 和步骤 3 可能发生重排序5. 分配对象内存6. 把对象引用赋值给 instance7. 初始化对象这样就可能出现instance 已经不为 null但对象还没有初始化完成。其他线程拿到这个对象后就可能出现异常行为。8. 正确写法publicclassSingleton{// volatile阻止重排序privatestaticvolatileSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton.class){if(instancenull){instancenewSingleton();}}}returninstance;}}这里 volatile 的作用就是禁止 instance new Singleton() 过程中的指令重排序避免其他线程拿到未初始化完成的对象。五、volatile 不能保证原子性这是 volatile 最容易被误解的地方。很多人以为变量加了 volatile 就线程安全了其实不是。例如publicclassCounter{privatevolatileintcount0;publicvoidadd(){count;}}虽然 count 使用了 volatile 修饰但 count 仍然不是线程安全的。因为 count 不是一步操作而是三个步骤读取 count 的值对 count 加 1把新值写回 count多个线程同时执行时可能出现数据覆盖。例如两个线程都读到 count 0线程 A读取 count 0线程 B读取 count 0线程 A计算 0 1写回 1线程 B计算 0 1写回 1两个线程都执行了自增但最终结果却是 1而不是 2。所以volatile 可以保证可见性但不能保证 i 这种复合操作的原子性六、如果要保证原子性怎么办如果需要保证 count 这种操作的线程安全可以使用下面几种方式。使用 synchronizedpublicclassCounter{privateintcount0;publicsynchronizedvoidadd(){count;}publicsynchronizedintgetCount(){returncount;}}synchronized 可以保证同一时刻只有一个线程执行同步方法因此可以保证原子性。2. 使用 Lockimportjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassCounter{privateintcount0;privatefinalLocklocknewReentrantLock();publicvoidadd(){lock.lock();try{count;}finally{lock.unlock();}}}Lock 比 synchronized 更灵活但代码也更复杂需要手动释放锁。3. 使用 AtomicIntegerimportjava.util.concurrent.atomic.AtomicInteger;publicclassCounter{privatefinalAtomicIntegercountnewAtomicInteger(0);publicvoidadd(){count.incrementAndGet();}publicintgetCount(){returncount.get();}}AtomicInteger 底层基于 CAS 实现适合做高并发下的原子自增操作。七、volatile 的常见使用场景状态标记privatevolatilebooleanrunningtrue;用于控制线程是否继续执行。适合场景线程停止标记开关控制任务取消标记2. 配置刷新privatevolatileStringconfig;一个线程更新配置其他线程读取最新配置。适合读多写少并且单次赋值即可完成更新的场景。3. 双重检查锁单例privatestaticvolatileSingletoninstance;用于防止对象创建过程中的指令重排序。4. 一次性发布对象引用privatevolatileUsercurrentUser;publicvoidupdateUser(Useruser){currentUseruser;}publicUsergetCurrentUser(){returncurrentUser;}如果对象本身构造完成后不再变化使用 volatile 发布引用可以让其他线程看到最新引用。八、volatile 不适合哪些场景volatile 不适合下面这些场景多个变量之间存在约束关系例如volatile int start;volatile int end;如果要求 start end 始终成立仅仅使用 volatile 不够因为它不能保证多个变量操作的整体一致性。复合操作例如count;这种操作需要读取、计算、写回volatile 不能保证整个过程不被其他线程打断。临界区代码如果一段代码中有多个操作必须作为一个整体执行应该使用synchronizedLock原子类并发容器而不是只使用 volatile。九、volatile 和 synchronized 的区别对比项 volatile synchronized是否保证可见性 是 是是否保证原子性 否 是是否禁止重排序 是 是是否加锁 不加锁 加锁是否会阻塞线程 不会 可能会性能开销 较小 相对较大使用场景 状态标记、配置刷新、对象发布 临界区、复合操作、复杂线程安全简单理解volatile 适合一个变量的简单读写synchronized 适合一段代码的互斥执行十、volatile 和 AtomicInteger 的区别对比项 volatile int AtomicInteger可见性 可以保证 可以保证原子自增 不能保证 可以保证底层机制 内存屏障 CAS适合场景 状态标记 计数器、并发累加示例对比private volatile int count 0;public void add() {count;}上面代码线程不安全。private AtomicInteger count new AtomicInteger(0);public void add() {count.incrementAndGet();}上面代码可以保证原子自增。十一、面试常见问题volatile 能保证线程安全吗不能完全保证。volatile 只能保证可见性和一定的有序性不能保证复合操作的原子性。如果只是简单的状态标记可以认为是线程安全的如果是 i 这种复合操作就不是线程安全的。volatile 为什么不能保证原子性因为原子性要求一个操作不可被中断。而 volatile 只能保证每次读到的是最新值不能保证读取、修改、写回这几个步骤作为一个整体执行。volatile 底层原理是什么可以从 Java 内存模型角度理解写 volatile 变量时会把变量刷新到主内存读 volatile 变量时会从主内存读取最新值通过内存屏障禁止特定类型的指令重排序双重检查锁为什么要用 volatile因为 new Object() 不是原子操作可能发生指令重排序。如果不加 volatile其他线程可能拿到一个还没有初始化完成的对象。volatile 可以替代 synchronized 吗不能完全替代。volatile 更轻量但能力有限synchronized 可以保证原子性、可见性和有序性适合更复杂的并发场景。十二、面试回答模板如果面试官问volatile 有什么作用可以这样回答volatile 是 Java 中的轻量级同步机制主要有两个作用第一是保证线程之间的可见性一个线程修改了 volatile 变量后其他线程可以看到最新值第二是禁止指令重排序比如双重检查锁单例中需要使用 volatile 防止对象还没有初始化完成就被其他线程拿到。但是 volatile 不能保证原子性比如 i 这种复合操作依然不是线程安全的。如果要保证原子性可以使用 synchronized、Lock 或者 AtomicInteger。如果面试官继续问volatile 适合什么场景可以这样回答volatile 适合变量之间没有复杂依赖关系并且操作本身比较简单的场景比如线程停止标记、开关控制、配置刷新、双重检查锁单例中的实例引用等。如果涉及多个操作组合或者需要保证复合操作的原子性就不适合只使用 volatile。十三、总结volatile 的重点可以总结为一句话volatile 保证可见性和有序性但不保证原子性。能力 volatile 是否支持可见性 支持禁止指令重排序 支持原子性 不支持线程阻塞 不会阻塞替代锁 不能完全替代实际开发中volatile 常用于状态标记、配置刷新和双重检查锁单例。如果遇到计数器、自增、自减、多个变量一致性更新等场景应该优先考虑 AtomicInteger、synchronized、Lock 或并发工具类。面试时只要抓住这几个关键词可见性禁止指令重排序不保证原子性状态标记双重检查锁i 不安全基本就能把 volatile 相关问题回答得比较完整。