Java面试题:volatile可以保证线程安全吗?
前两天刷面试题的时候,又看到一个老生常谈的问题:“volatile能不能保证线程安全吗?”
别急着回答,我也知道这个话题已经被讨论得滚瓜烂熟了,但今天咱们用程序员的视角,再扒拉一下这个问题背后的门道,顺便聊聊那些面试场上常见的“坑”。
volatile 到底是个啥?
先别着急,咱们先来回顾一下 volatile 的基本作用。如果用人话翻译,volatile 这个关键字的主要工作就是两个:
保证可见性:一个线程修改了变量,其他线程能够立刻“看到”最新的值。 禁止指令重排序:编译器和 CPU 不会随意调整使用了 volatile修饰的变量的读写顺序。
听着挺强,是吧?但别高兴得太早,这里“保证可见性”和“禁止重排序”,和线程安全可完全不是一回事。别急,接下来我用一个小例子帮大家理清楚。
volatile 的实际表现
假设我们有个共享变量 counter,两个线程分别对它进行加 1 操作,代码大概是这样的:
class Counter {
volatile int count = 0; public void increment() {
count++;
}
}
现在问题来了,这段代码线程安全吗?你要是回答“安全”,面试官估计都懒得跟你多说话了。因为这段代码实际上根本不安全。
为什么?🤔
虽然 volatile 保证了 count 的可见性,但 count++ 不是一个原子操作。它其实可以分解成以下三步:
读取 count的值;对这个值加 1; 将新值写回 count。
多线程情况下,线程 A 和线程 B 的这三步操作可能交叉执行,比如:
线程 A 读取了 count的值为 10;线程 B 也读取了 count的值为 10;线程 A 将 count更新为 11;线程 B 将 count更新为 11。
结果呢?两个线程干了两次活,count 只加了一次,典型的线程安全问题。💣
如何让它变得线程安全?
知道问题在哪,解决起来就简单了。这里有几个常用的方法:
1. 使用 synchronized
直接给 increment 方法加锁,保证同一时间只有一个线程能执行这段逻辑。
class Counter {
private int count = 0; public synchronized void increment() {
count++;
}
}
这个方法简单粗暴,但性能一般,尤其是线程竞争激烈时,锁的开销会很大。
2. 使用 AtomicInteger
AtomicInteger 是 JDK 提供的线程安全类,内部实现用了 CAS(Compare and Swap)机制,效率比 synchronized 高。
import java.util.concurrent.atomic.AtomicInteger;class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
}
这里的 incrementAndGet 是原子操作,不会出现线程安全问题。
3. 使用 Lock
如果需要更灵活的锁机制,比如读写锁,可以使用 ReentrantLock。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
这种方法适用于更复杂的场景,但写起来略微繁琐。
volatile 的适用场景
既然 volatile 不能保证线程安全,那它到底适合用在哪些地方呢?其实 volatile 的强项是在以下几种场景中:
1. 状态标志
比如实现一个简单的开关,控制线程的停止:
class Task implements Runnable {
private volatile boolean running = true; public void run() {
while (running) {
// 执行任务
}
}
public void stop() {
running = false;
}
}
这里 volatile 保证了 running 的可见性,stop 方法在一个线程中被调用后,run 方法中的循环能立刻感知到变化。
2. 单例模式中的双重检查锁
实现单例模式时,经常会看到这样的代码:
class Singleton {
private static volatile Singleton instance; private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里 volatile 的作用是防止指令重排序,保证对象初始化的正确性。
面试中的“坑”
面试官问这个问题的时候,很多人可能会一股脑地回答:“volatile 不保证线程安全!”但你要注意,面试官更想看的是你的分析能力,而不是简单的背答案。
比如,可以这样回答:
先阐述 volatile的作用:可见性和防止重排序。说明它的局限性:不保证复合操作的原子性。 提出解决方案: synchronized、AtomicInteger等。分析适用场景:状态标志、双重检查锁等。
如果还能现场写几行代码示例,那基本就稳了。😎
总结
volatile 是个不错的工具,但它的适用场景非常有限。面试的时候,不要简单地把它当成“万能药”。理解它的底层机制和适用场景,才能避免“掉坑”。
那么,大家在工作中有遇到过类似的线程安全问题吗?欢迎留言交流,说不定你的经历就是下一个面试加分项!🚀
-END-
以上,就是今天的分享了,看完文章记得右下角给何老师点赞,也欢迎在评论区写下你的留言。