volatile 实战应用篇 —— 典型场景

一、前言

在上一篇文章中,我们深入剖析了 volatile 的底层原理,知道它通过强化 JMM 交互规则保证可见性,通过插入内存屏障保证有序性,但不具备原子性。

本篇文章将聚焦 volatile 的实战应用,带你掌握它的核心适用场景、避坑指南,以及与其他同步方案的性能对比。

二、核心适用原则

在使用 volatile 之前,我们必须牢记一个核心原则:volatile 仅适用于「单一赋值、多线程读取」的场景,不适合用于需要原子性的复合操作场景。

这个原则是判断 volatile 是否适用的根本依据。简单来说,当共享变量的操作满足以下两个条件时,才能用 volatile :

  • **操作是「单一赋值」:**比如 flag = true 、 status = 1 ,而不是 count++ 、 value += 1 这类复合操作;
  • **变量不依赖自身的旧值:**赋值时不需要读取变量的旧值来计算新值。

违背这个原则使用 volatile ,必然会导致线程安全问题。

三、典型应用场景

场景 1:状态标志位(最经典场景)

用 volatile 修饰布尔类型或枚举类型的状态变量,作为线程的启停开关、状态标记,是它最常见也最安全的用法。这种场景完全符合「单一赋值、多线程读取」的原则。

实战案例:线程优雅启停

我们在上一篇文章中用到的线程启停开关,就是一个典型案例。这里我们做一个更贴近实战的升级版本 ------ 用 volatile 控制后台任务线程的启停:

java 复制代码
import java.util.concurrent.TimeUnit;
public class VolatileStatusFlagDemo {
    // 用 volatile 修饰状态标志位
    private volatile boolean isTaskRunning = false;
    private Thread backgroundThread;
    // 启动后台任务
    public void startBackgroundTask() {
        if (isTaskRunning) {
            System.out.println("任务已经在运行中");
            return;
        }
        isTaskRunning = true;
        backgroundThread = new Thread(() -> {
            while (isTaskRunning) {
                // 执行业务逻辑:比如定时拉取数据
                System.out.println("后台任务正在执行...");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
            System.out.println("后台任务已停止");
        });
        backgroundThread.start();
    }
    // 停止后台任务
    public void stopBackgroundTask() {
        isTaskRunning = false;
        // 唤醒可能处于休眠状态的线程
        if (backgroundThread != null) {
            backgroundThread.interrupt();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        VolatileStatusFlagDemo demo = new VolatileStatusFlagDemo();
        // 启动任务
        demo.startBackgroundTask();
        // 运行 5 秒后停止
        TimeUnit.SECONDS.sleep(5);
        demo.stopBackgroundTask();
    }
}

代码解析 :

  • isTaskRunning 被 volatile 修饰,主线程调用 stopBackgroundTask() 修改其值时,后台线程能立即感知到,从而退出循环;

  • 相比 synchronized ,这种方式无需加锁,性能开销极低,是实现线程状态控制的最优解。

场景 2:双重检查锁定(DCL)单例模式(必考点)

DCL 单例模式是面试中的高频考点,而 volatile 是这个模式中必不可少的关键角色。没有 volatile 的 DCL 单例模式,在多线程环境下会存在严重的线程安全问题。

问题版本:无 volatile 的 DCL 单例

java 复制代码
public class UnsafeSingleton {
    private static UnsafeSingleton instance; // 未加 volatile
    private UnsafeSingleton() {}
    public static UnsafeSingleton getInstance() {
        // 第一次检查:避免不必要的锁竞争
        if (instance == null) {
            synchronized (UnsafeSingleton.class) {
                // 第二次检查:防止多线程重复创建实例
                if (instance == null) {
                    instance = new UnsafeSingleton(); // 存在指令重排序风险
                }
            }
        }
        return instance;
    }
}

风险分析 :

  • instance = new UnsafeSingleton() 会被拆分为「分配内存→初始化对象→赋值引用」三步;

  • JVM 可能对后两步重排序,变成「分配内存→赋值引用→初始化对象」;

  • 当线程 A 执行完「赋值引用」后, instance 已经不为 null ,但对象还未初始化;此时线程 B 第一次检查会认为 instance 已创建,直接返回一个未初始化的实例,引发空指针异常。

正确版本:加 volatile 的 DCL 单例

java 复制代码
public class SafeSingleton {
    // 必须加 volatile 禁止指令重排序
    private static volatile SafeSingleton instance;
    private SafeSingleton() {}
    public static SafeSingleton getInstance() {
        if (instance == null) {
            synchronized (SafeSingleton.class) {
                if (instance == null) {
                    instance = new SafeSingleton();
                }
            }
        }
        return instance;
    }
}

核心作用 :

  • volatile 禁止了 instance = new SafeSingleton() 的指令重排序,确保「初始化对象」一定在「赋值引用」之前完成;

  • 线程 B 只有在对象完全初始化后,才会看到 instance 不为 null ,彻底避免了半初始化实例的问题。

场景 3:轻量级数据传递(低并发场景)

在低并发场景下,用 volatile 修饰简单的数值类型变量,实现多线程之间的数据传递,也是一种常见用法。比如主线程向工作线程传递配置参数、阈值等。

实战案例:动态调整阈值

java 复制代码
public class VolatileThresholdDemo {
    // 用 volatile 修饰阈值,支持动态调整
    private volatile int threshold = 100;
    public void startWorker() {
        new Thread(() -> {
            int count = 0;
            while (true) {
                count++;
                // 读取最新的阈值
                if (count > threshold) {
                    System.out.println("count 超过阈值:" + threshold + ",重置 count");
                    count = 0;
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }).start();
    }
    // 主线程动态调整阈值
    public void updateThreshold(int newThreshold) {
        this.threshold = newThreshold;
        System.out.println("阈值已更新为:" + newThreshold);
    }
    public static void main(String[] args) throws InterruptedException {
        VolatileThresholdDemo demo = new VolatileThresholdDemo();
        demo.startWorker();
        // 运行 2 秒后调整阈值
        TimeUnit.SECONDS.sleep(2);
        demo.updateThreshold(200);
        // 再运行 2 秒后调整阈值
        TimeUnit.SECONDS.sleep(2);
        demo.updateThreshold(50);
    }
}

代码解析 :

  • 工作线程会实时读取 threshold 的最新值,无需加锁;

  • 这种用法仅适用于低并发、无复合操作的场景,若存在多线程同时修改 threshold 的情况,需要搭配 AtomicInteger 使用。

四、常见坑点及规避方案

坑点 1:误用 volatile 修饰需要原子性的变量

**表现 :**用 volatile 修饰 count 等需要累加的变量,执行 count++ 操作,导致最终结果小于预期值。

原因 : count++ 是复合操作, volatile 无法保证其原子性,多线程同时操作会出现数据覆盖问题。

规避方案 :

  • 替换为 AtomicInteger 等原子类,利用 CAS 机制保证原子性;
  • 或使用 synchronized 修饰操作方法。

优化示例 :

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCountDemo {
    // 用 AtomicInteger 替代 volatile int
    private final AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        count.incrementAndGet(); // 原子操作
    }
    public int getCount() {
        return count.get();
    }
}

坑点 2:volatile 修饰对象引用,误以为能保证对象内部字段的可见性

**表现 :**用 volatile 修饰对象引用 user ,修改 user 的内部字段 user.setName("张三") ,其他线程无法感知到字段变化。

原因 : volatile 仅保证对象引用本身的可见性,不保证对象内部字段的可见性。当对象引用未发生变化时,内部字段的修改不会触发 volatile 的同步机制。

规避方案 :

  • 若需要修改对象内部字段,可将字段单独用 volatile 修饰;
  • 或使用 synchronized 修饰修改字段的方法;
  • 或每次修改字段时,重新创建一个新的对象实例(适用于不可变对象)。

示例说明 :

java 复制代码
class User {
    // 内部字段单独用 volatile 修饰
    private volatile String name;
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}
public class VolatileObjectDemo {
    private volatile User user = new User();
    public void updateUserName() {
        user.setName("张三"); // 其他线程能感知到 name 的变化
    }
}

坑点 3:认为 volatile 可以替代 synchronized

**表现 :**在高并发、复合操作场景下,用 volatile 替代 synchronized ,导致线程安全问题。

原因 : volatile 仅保证可见性和有序性,不保证原子性;而 synchronized 能保证原子性、可见性、有序性三者。

**规避方案 :**牢记两者的适用边界,用表格总结如下:

| 场景类型 | 推荐方案 | 不推荐方案 |
| 状态标志位、单例模式 DCL | volatile | synchronized |
| 复合操作(如 count++) | synchronized / 原子类 | volatile |

复杂临界区代码 synchronized volatile

五、volatile与synchronized的性能对比

为了更直观地看到 volatile 的性能优势,我们做一个简单的性能测试 ------ 对比两者在状态标记场景下的执行效率。

测试代码

java 复制代码
import java.util.concurrent.CountDownLatch;
public class VolatileVsSyncPerformanceTest {
    private static volatile boolean volatileFlag = false;
    private static boolean syncFlag = false;
    private static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 1000;
        CountDownLatch volatileLatch = new CountDownLatch(threadCount);
        CountDownLatch syncLatch = new CountDownLatch(threadCount);
        // 测试 volatile
        long volatileStart = System.currentTimeMillis();
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                while (!volatileFlag) {}
                volatileLatch.countDown();
            }).start();
        }
        volatileFlag = true;
        volatileLatch.await();
        long volatileEnd = System.currentTimeMillis();
        System.out.println("volatile 耗时:" + (volatileEnd - volatileStart) + "ms");
        // 测试 synchronized
        long syncStart = System.currentTimeMillis();
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                while (true) {
                    synchronized (lock) {
                        if (syncFlag) {
                            break;
                        }
                    }
                }
                syncLatch.countDown();
            }).start();
        }
        synchronized (lock) {
            syncFlag = true;
        }
        syncLatch.await();
        long syncEnd = System.currentTimeMillis();
        System.out.println("synchronized 耗时:" + (syncEnd - syncStart) + "ms");
    }
}

测试结果(仅供参考,不同环境结果不同)

java 复制代码
volatile 耗时:5ms
synchronized 耗时:32ms

结果分析

  • volatile 是无锁机制,仅通过内存屏障保证同步,性能开销极低;
  • synchronized 在低并发场景下会升级为偏向锁 / 轻量级锁,但仍有锁竞争的开销;
  • 在状态标记这类简单场景下, volatile 的性能远超 synchronized 。

六、总结

通过本文的学习,我们掌握了 volatile 的核心适用场景和避坑指南。那么问题来了:

  • 如果在高并发场景下,需要同时保证可见性和原子性,除了 synchronized 和原子类,还有什么更高效的方案?
  • volatile 修饰的变量和 Atomic 类的变量,在底层实现上有什么区别?

在下一篇文章中,我们将聚焦 volatile 的选型对比,深入分析它与 synchronized 、 Atomic 类的核心差异,帮你在实战中做出最优的技术选型。

相关推荐
xie_pin_an1 小时前
从二叉搜索树到哈希表:四种常用数据结构的原理与实现
java·数据结构
没有bug.的程序员2 小时前
Java 并发容器深度剖析:ConcurrentHashMap 源码解析与性能优化
java·开发语言·性能优化·并发·源码解析·并发容器
kk哥88993 小时前
分享一些学习JavaSE的经验和技巧
java·开发语言
栈与堆3 小时前
LeetCode 21 - 合并两个有序链表
java·数据结构·python·算法·leetcode·链表·rust
lagrahhn3 小时前
Java的RoundingMode舍入模式
java·开发语言·金融
鸽鸽程序猿3 小时前
【JavaEE】【SpringCloud】注册中心_nacos
java·spring cloud·java-ee
云上凯歌3 小时前
01 GB28181协议基础理解
java·开发语言
Coder_Boy_4 小时前
基于SpringAI的在线考试系统-考试系统DDD(领域驱动设计)实现步骤详解
java·数据库·人工智能·spring boot
毕设源码-钟学长4 小时前
【开题答辩全过程】以 基于Java的运动器材销售网站为例,包含答辩的问题和答案
java·开发语言