程序员老鬼

Java面试题:volatile可以保证线程安全吗?

前两天刷面试题的时候,又看到一个老生常谈的问题:“volatile能不能保证线程安全吗?”

别急着回答,我也知道这个话题已经被讨论得滚瓜烂熟了,但今天咱们用程序员的视角,再扒拉一下这个问题背后的门道,顺便聊聊那些面试场上常见的“坑”。

volatile 到底是个啥?

先别着急,咱们先来回顾一下 volatile 的基本作用。如果用人话翻译,volatile 这个关键字的主要工作就是两个:

  1. 保证可见性:一个线程修改了变量,其他线程能够立刻“看到”最新的值。
  2. 禁止指令重排序:编译器和 CPU 不会随意调整使用了 volatile 修饰的变量的读写顺序。

听着挺强,是吧?但别高兴得太早,这里“保证可见性”和“禁止重排序”,和线程安全可完全不是一回事。别急,接下来我用一个小例子帮大家理清楚。


volatile 的实际表现

假设我们有个共享变量 counter,两个线程分别对它进行加 1 操作,代码大概是这样的:

class Counter {
    volatile int count = 0;

    public void increment() {
        count++;
    }
}

现在问题来了,这段代码线程安全吗?你要是回答“安全”,面试官估计都懒得跟你多说话了。因为这段代码实际上根本不安全。

为什么?🤔

虽然 volatile 保证了 count 的可见性,但 count++ 不是一个原子操作。它其实可以分解成以下三步:

  1. 读取 count 的值;
  2. 对这个值加 1;
  3. 将新值写回 count。

多线程情况下,线程 A 和线程 B 的这三步操作可能交叉执行,比如:

  1. 线程 A 读取了 count 的值为 10;
  2. 线程 B 也读取了 count 的值为 10;
  3. 线程 A 将 count 更新为 11;
  4. 线程 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 不保证线程安全!”但你要注意,面试官更想看的是你的分析能力,而不是简单的背答案。

比如,可以这样回答:

  1. 先阐述 volatile 的作用:可见性和防止重排序。
  2. 说明它的局限性:不保证复合操作的原子性。
  3. 提出解决方案:synchronized、AtomicInteger 等。
  4. 分析适用场景:状态标志、双重检查锁等。

如果还能现场写几行代码示例,那基本就稳了。😎


总结

volatile 是个不错的工具,但它的适用场景非常有限。面试的时候,不要简单地把它当成“万能药”。理解它的底层机制和适用场景,才能避免“掉坑”。

那么,大家在工作中有遇到过类似的线程安全问题吗?欢迎留言交流,说不定你的经历就是下一个面试加分项!🚀

-END-

ok,今天先说到这,老规矩,给大家分享一份不错的副业资料,感兴趣的同学找我领取。

Image

以上,就是今天的分享了,看完文章记得右下角给何老师点赞,也欢迎在评论区写下你的留言。