Java volatile 关键字相关用法总结:面试版详解
前言
在 Java 多线程面试中,volatile 是一个非常高频的关键字。它看起来简单,只是在变量前面加一个修饰符,但背后涉及 Java 内存模型、线程可见性、指令重排序、原子性等并发基础。
面试官常见问法有:
- volatile 有什么作用?
- volatile 能保证线程安全吗?
- volatile 和 synchronized 有什么区别?
- 为什么双重检查锁单例要加 volatile?
- volatile 能不能保证 i++ 的原子性?
这篇文章从面试角度系统总结一下 volatile 的相关用法。
一、volatile 是什么?
volatile 是 Java 提供的一个轻量级同步机制,可以用来修饰变量。
基本写法如下:
privatevolatilebooleanflag=true;它主要有两个核心作用:
- 保证线程之间的可见性
- 禁止指令重排序
但是需要特别注意:
volatile 不能保证复合操作的原子性
这句话是面试里的重点。
二、为什么需要 volatile?
在多线程环境下,每个线程可能会把共享变量从主内存复制到自己的工作内存中操作。
如果一个线程修改了共享变量,另一个线程不一定能立刻看到最新值。
可以简单理解为:
- 主内存:保存共享变量
- 线程工作内存:线程自己使用的变量副本
如果没有同步机制,可能出现下面的问题:
publicclassVolatileDemo{privatestaticbooleanrunning=true;publicstaticvoidmain(String[]args)throwsInterruptedException{newThread(()->{while(running){// 执行任务}System.out.println("线程停止");}).start();Thread.sleep(1000);running=false;}}理论上,主线程把 running 改成 false 后,子线程应该停止。
但如果 running 没有使用 volatile 修饰,子线程可能一直读取自己工作内存中的旧值,导致循环无法结束。
修改后:
publicclassVolatileDemo{privatestaticvolatilebooleanrunning=true;publicstaticvoidmain(String[]args)throwsInterruptedException{newThread(()->{while(running){// 执行任务}System.out.println("线程停止");}).start();Thread.sleep(1000);running=false;}}加上 volatile 后,一个线程修改变量,其他线程可以更及时地看到最新值。
三、volatile 的第一个作用:保证可见性
可见性指的是:
一个线程修改了共享变量的值,其他线程能够立刻看到这个修改。
使用 volatile 修饰变量后:
对 volatile 变量的写操作会立即刷新到主内存
对 volatile 变量的读操作会从主内存读取最新值
典型场景是线程停止标记:
publicclassTaskimplementsRunnable{privatevolatilebooleanstopped=false;publicvoidstop(){stopped=true;}@Overridepublicvoidrun(){while(!stopped){System.out.println("任务执行中");}System.out.println("任务已停止");}}这种场景下,volatile 非常合适,因为 stopped 只是一个状态标记,读写操作都很简单。
四、volatile 的第二个作用:禁止指令重排序
为了提高执行效率,编译器和 CPU 可能会对指令进行重排序。
在单线程环境下,重排序不会影响最终结果;但在多线程环境下,重排序可能导致线程安全问题。
最经典的例子就是双重检查锁单例模式。
- 不加 volatile 的问题 - 懒汉式情况
publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}}returninstance;}}看起来这段代码已经使用了 synchronized,但仍然可能有问题。
因为创建对象并不是一个简单操作,大致可以拆成三步:
2. 分配对象内存
3. 初始化对象
4. 把对象引用赋值给 instance
在某些情况下,步骤 2 和步骤 3 可能发生重排序:
5. 分配对象内存
6. 把对象引用赋值给 instance
7. 初始化对象
这样就可能出现:instance 已经不为 null,但对象还没有初始化完成。其他线程拿到这个对象后,就可能出现异常行为。
8. 正确写法
publicclassSingleton{// volatile阻止重排序privatestaticvolatileSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}}returninstance;}}这里 volatile 的作用就是禁止 instance = new Singleton() 过程中的指令重排序,避免其他线程拿到未初始化完成的对象。
五、volatile 不能保证原子性
这是 volatile 最容易被误解的地方。
很多人以为变量加了 volatile 就线程安全了,其实不是。
例如:
publicclassCounter{privatevolatileintcount=0;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++ 这种操作的线程安全,可以使用下面几种方式。
- 使用 synchronized
publicclassCounter{privateintcount=0;publicsynchronizedvoidadd(){count++;}publicsynchronizedintgetCount(){returncount;}}synchronized 可以保证同一时刻只有一个线程执行同步方法,因此可以保证原子性。
2. 使用 Lock
importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassCounter{privateintcount=0;privatefinalLocklock=newReentrantLock();publicvoidadd(){lock.lock();try{count++;}finally{lock.unlock();}}}Lock 比 synchronized 更灵活,但代码也更复杂,需要手动释放锁。
3. 使用 AtomicInteger
importjava.util.concurrent.atomic.AtomicInteger;publicclassCounter{privatefinalAtomicIntegercount=newAtomicInteger(0);publicvoidadd(){count.incrementAndGet();}publicintgetCount(){returncount.get();}}AtomicInteger 底层基于 CAS 实现,适合做高并发下的原子自增操作。
七、volatile 的常见使用场景
- 状态标记
privatevolatilebooleanrunning=true;用于控制线程是否继续执行。
适合场景:
线程停止标记
开关控制
任务取消标记
2. 配置刷新
privatevolatileStringconfig;一个线程更新配置,其他线程读取最新配置。
适合读多写少,并且单次赋值即可完成更新的场景。
3. 双重检查锁单例
privatestaticvolatileSingletoninstance;用于防止对象创建过程中的指令重排序。
4. 一次性发布对象引用
privatevolatileUsercurrentUser;publicvoidupdateUser(Useruser){currentUser=user;}publicUsergetCurrentUser(){returncurrentUser;}如果对象本身构造完成后不再变化,使用 volatile 发布引用可以让其他线程看到最新引用。
八、volatile 不适合哪些场景?
volatile 不适合下面这些场景:
- 多个变量之间存在约束关系
例如:
volatile int start;
volatile int end;
如果要求 start <= end 始终成立,仅仅使用 volatile 不够,因为它不能保证多个变量操作的整体一致性。 - 复合操作
例如:
count++;
这种操作需要读取、计算、写回,volatile 不能保证整个过程不被其他线程打断。 - 临界区代码
如果一段代码中有多个操作必须作为一个整体执行,应该使用:
synchronized
Lock
原子类
并发容器
而不是只使用 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 相关问题回答得比较完整。
