Java volatile

1. 基础概念

1.1 什么是 volatile?

volatile 是 Java 中的一个关键字,用于修饰变量。它保证了变量的可见性有序性 ,但不保证原子性

java 复制代码
public class VolatileExample {
    // 普通变量
    private int normalVar = 0;
    
    // volatile 变量
    private volatile int volatileVar = 0;
    
    // volatile 引用类型
    private volatile Object obj = new Object();
}

1.2 volatile 的三大特性

  1. 可见性(Visibility):当一个线程修改了 volatile 变量的值,其他线程能够立即看到这个修改。
  2. 有序性(Ordering):禁止指令重排序优化。
  3. 非原子性(Non-Atomicity):volatile 不能保证复合操作的原子性。

2. volatile 的底层实现机制

2.1 JMM 内存划分

JMM 中将内存划分为主内存和工作内存。

线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内 存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

点击阅读:JVM(一)------ JVM内存管理

2.2 指令重排序问题

为了提高程序执行效率,编译器和 CPU 可以改变指令的执行顺序,单线程程序的结果不变。目的是利用 CPU 并行能力、流水线优化、减少内存访问延迟,提高性能。

java 复制代码
/**
 * 【底层原理】编译器和 CPU 为了优化性能会重新排列指令执行顺序
 */
public class ReorderingProblem {
    private int a = 0;
    private boolean flag = false;
    
    // 线程 1 执行
    public void writer() {
        a = 1;          // 操作 1
        flag = true;    // 操作 2
        
        // 【问题】编译器或 CPU 可能重排序为:
        // flag = true;    // 操作 2 先执行
        // a = 1;          // 操作 1 后执行
    }
    
    // 线程 2 执行
    public void reader() {
        if (flag) {     // 如果看到 flag = true
            // 【问题】可能看到 a = 0(重排序导致)
            System.out.println("a = " + a);
        }
    }
}

2.3 内存屏障(Memory Barrier)

内存屏障是一种指令级机制,用于控制 CPU 或编译器的指令执行顺序,并保证多线程访问共享变量的可见性和顺序性。

java 复制代码
/**
 * volatile 变量的读写操作会插入内存屏障
 * 【底层原理】JVM 会在 volatile 变量操作前后插入特定的内存屏障指令。通过内存屏障强制刷新缓存,保证可见性
 */
public class MemoryBarrierExample {
    private volatile boolean flag = false;
    private int data = 0;
    
    public void writer() {
        data = 42;          // 1. 普通写操作
        // StoreStore 屏障 - 确保上面的写操作在 volatile 写之前完成
        flag = true;        // 2. volatile 写操作
        // StoreLoad 屏障 - 确保 volatile 写操作立即刷新到主内存
    }
    
    public void reader() {
        // LoadLoad 屏障 - 确保 volatile 读操作从主内存获取最新值
        if (flag) {         // 1. volatile 读操作
            // LoadStore 屏障 - 确保后续读操作能看到最新的数据
            System.out.println(data); // 2. 能够看到 data = 42
        }
    }
}

四种内存屏障类型:

屏障类型 位置 作用
StoreStore volatile 写之前 确保该线程之前的普通写操作(如 data=42)都已经写入工作内存或刷新到主内存,不能被重排到 volatile 写之后
StoreLoad volatile 写之后 保证 volatile 写立即刷新到主内存,并阻止 volatile 写与随后的读/写重排
LoadLoad volatile 读之前 确保 volatile 读之前的读操作完成,防止读操作重排到 volatile 读之后
LoadStore volatile 读之后 确保 volatile 读完成后,后续读/写操作不会被重排到 volatile 读之前

happens-before 原则:所有在 volatile 写之前的操作都对后续的 volatile 读可见

2.4 不保证原子性

复合操作 = 多步骤操作,例如:

java 复制代码
counter++; // 等价于 read -> modify -> write

Step 1: 从主内存/工作内存读 counter

Step 2: 自增操作(+1)

Step 3: 写回工作内存/主内存

问题 :多个线程同时执行 counter++,可能出现:

  1. 线程 A 读到 5
  2. 线程 B 也读到 5
  3. A 写回 6
  4. B 写回 6

结果:虽然两个线程都执行了 ++,counter 只增加了 1 → 数据丢失

volatile 只保证每次读/写最新可见,不保证多个操作的不可分割性

3. 应用场景

3.1 单例模式(双重检查锁定)

为什么 instance 必须用 volatile 修饰?在双重检查锁定单例模式中,instance 必须用 volatile 修饰,否则可能因为 指令重排序 导致返回"未初始化完成"的对象,从而破坏单例的线程安全性。

java 复制代码
/**
 * 【经典应用】双重检查锁定单例模式
 */
public class Singleton {
    // 必须使用 volatile 修饰
    // 防止指令重排序导致的半初始化对象问题
    private static volatile Singleton instance;
    
    private Singleton() {
    }
    
    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {            // 第二次检查
                    // 【问题】对象创建分为三个步骤:
                    // 1. 分配内存空间
                    // 2. 初始化对象
                    // 3. 将引用指向内存空间
                    // 
                    // 【重排序风险】步骤 2 和 3 可能被重排序
                    // 【后果】其他线程可能看到未完全初始化的对象
                    instance = new Singleton();       // 可能发生重排序
                }
            }
        }
        return instance;
    }
}

/**
 * 【错误示例】不使用 volatile 的问题
 */
class ProblematicSingleton {
    private static ProblematicSingleton instance; // 没有 volatile
    
    public static ProblematicSingleton getInstance() {
        if (instance == null) {
            synchronized (ProblematicSingleton.class) {
                if (instance == null) {
                    // 【问题场景】
                    // 线程 A:执行到步骤 3,instance 不为 null,但对象未初始化
                    // 线程 B:看到 instance != null,直接返回未初始化的对象
                    // 正常顺序:分配内存 → 初始化 → 引用指向
                    // 重排序后的顺序:分配内存 → 引用指向(instance != null) → 初始化
                    // 【结果】程序崩溃或产生不可预期的行为
                    instance = new ProblematicSingleton();
                }
            }
        }
        return instance;
    }
}

3.2 状态标志

java 复制代码
/**
 * 【应用场景】线程间状态通信
 * 【适用条件】简单的布尔状态标志
 */
public class StatusFlag {
    // 【用途】控制线程的启动和停止
    private volatile boolean running = true;
    private volatile boolean initialized = false;
    
    public void startWorker() {
        new Thread(() -> {
            // 等待初始化完成
            while (!initialized) {
                // 【volatile 读】确保能及时看到初始化完成的信号
                Thread.yield();
            }
            
            // 执行工作循环
            while (running) {
                // 【volatile 读】确保能及时响应停止信号
                doWork();
            }
            
            cleanup();
        }).start();
    }
    
    public void initialize() {
        // 执行初始化逻辑
        setupResources();
        
        // 【volatile 写】通知工作线程初始化完成
        initialized = true;
    }
    
    public void stop() {
        // 【volatile 写】发送停止信号
        running = false;
    }
    
    private void doWork() {
        // 模拟工作
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
   
}
相关推荐
叫我阿柒啊3 小时前
Java全栈工程师的面试实战:从基础到复杂问题的完整解析
java·数据库·spring boot·微服务·vue3·测试·全栈开发
CYRUS_STUDIO3 小时前
别再手工写 Hook 了!Python + Frida 一网打尽 SO 层动态注册 JNI 调用
android·c++·逆向
花花无缺3 小时前
函数和方法的区别
java·后端·python
赵星星5203 小时前
深入理解Spring的@TransactionalEventListener:事务与事件的完美协作
java
Boblim3 小时前
spark streaming消费rocketmq的几种方式
java
天天摸鱼的java工程师3 小时前
别再只会 new 了!八年老炮带你看透对象创建的 5 层真相
java·后端
洛阳泰山3 小时前
MaxKB4j智能体平台 Docker Compose 快速部署教程
java·人工智能·后端
南北是北北3 小时前
Flow 热流
前端·面试
渣哥4 小时前
Java 为啥偏偏不让多重继承?
java