volatile 关键字

volatile关键字的基本概念

一、问题背景:CPU 缓存带来的可见性问题

现代 CPU 为了提升性能,每个核心都有自己的高速缓存(Cache)。线程运行在不同核心上时,读取变量可能读的是各自缓存里的副本,而不是主内存中的最新值。


1.1 没有 volatile 时的代码

java 复制代码
public class VisibilityProblem {
    // 普通变量,无 volatile
    private boolean running = true;

    public void stop() {
        running = false;  // 线程 A 修改
    }

    public void doWork() {
        while (running) {  // 线程 B 读取
            // 执行任务...
        }
        System.out.println("已停止");
    }
}

问题场景:

  • 线程 A 调用 stop(),把 running 改为 false

  • 线程 B 之前读取的是 running = ture, 导致 while (running) 循环可能永远停不下来

1.2 为什么会这样?

复制代码
┌─────────────┐         ┌─────────────┐
│   线程 A    │         │   线程 B    │
│  (核心 1)   │         │  (核心 2)   │
└──────┬──────┘         └──────┬──────┘
       │                       │
       ▼                       ▼
┌──────────────┐        ┌──────────────┐
│  缓存副本 A   │        │  缓存副本 B   │
│ running=false │        │ running=true  │  ← 线程 B 还在读旧值!
└──────┬───────┘        └──────┬───────┘
       │                       │
       └───────────┬───────────┘
                   ▼
            ┌──────────────┐
            │    主内存     │
            │ running=false│  ← 线程 A 已写入,但线程 B 的缓存未失效
            └──────────────┘

每个线程都有自己的工作内存:

java 复制代码
        主内存

        flag=false
           |
  -------------------
  |                 |
线程A缓存       线程B缓存

flag=false      flag=true

线程 A 修改:flag=false,可能只是修改了自己的缓存,没有及时刷新到主内存。

核心问题: 线程 A 修改了主内存的值,但线程 B 的缓存副本不会自动失效,导致线程 B 看不到最新值

这就是 可见性(Visibility)问题


二、volatile 的诞生:解决可见性

Java 设计者意识到:程序员需要一个关键字,告诉 JVM "这个变量的每次读写都必须直接操作主内存,不要依赖缓存"

于是 volatile 被创建出来。

2.1 加上 volatile 后

java 复制代码
public class VisibilityFixed {
    // 加上 volatile
    private volatile boolean running = true;

    public void stop() {
        running = false;  // 线程 A 写入 → 直接刷回主内存
    }

    public void doWork() {
        while (running) {  // 线程 B 读取 → 直接从主内存重新加载
            // 执行任务...
        }
        System.out.println("已停止");
    }
}

效果:

  • 线程 A 写入 running = false 时,会立即刷新到主内存

  • 线程 B 读取 running 时,会使本地缓存失效,重新从主内存读取

这样线程 B 就能立刻看到 false,循环正常退出。


三、volatile 的第二个作用:禁止指令重排序

3.1 什么是指令重排序?

编译器和 CPU 为了提高执行效率,可能会调整代码的执行顺序

在单线程下这没问题,但在多线程下可能引发灾难。

3.2 没有 volatile 时的重排序问题

java 复制代码
public class ReorderingProblem {
    private int value = 0;
    private boolean ready = false;

    // 线程 A 执行
    public void writer() {
        value = 42;          // ①
        ready = true;        // ②
    }

    // 线程 B 执行
    public void reader() {
        if (ready) {         // ③
            System.out.println(value);  // ④
        }
    }
}

没有 volatile 时,编译器/CPU 可能把 ① 和 ② 重排序:

java 复制代码
// 实际执行顺序可能变成:
ready = true;        // ② 先执行
value = 42;          // ① 后执行

后果:

  • 线程 B 看到 ready == true,但 value 还是 0

  • 程序输出 0,而不是预期的 42

3.3 volatile 如何禁止重排序?

volatile 会在读写操作前后插入内存屏障(Memory Barrier),强制保证执行顺序:

复制代码
线程 A 的 writer():
    value = 42;
    ──[StoreStore 屏障]──  // 保证 value 的写入在 ready 之前
    ready = true;          // volatile 写
    ──[StoreLoad 屏障]──  // 保证此操作之前的写入对其他线程可见

线程 B 的 reader():
    if (ready) {           // volatile 读
    ──[LoadLoad 屏障]──   // 保证 ready 的读取在 value 之前
        System.out.println(value);
    }

内存屏障的本质: 一道"栅栏",阻止指令越过它进行重排序。


四、volatile 不能做什么?(重要)

4.1 volatile 不能保证原子性

java 复制代码
public class NotAtomic {
    private volatile int count = 0;

    // 线程 A 和线程 B 同时执行
    public void increment() {
        count++;  // 这不是原子操作!
    }
}

count++ 实际上分三步:

  1. 读取 count 的值

  2. 加 1

  3. 写回 count

即使加了 volatile,这三步之间仍可能被其他线程打断:

复制代码
时间点  线程 A              线程 B
  t1   读取 count=0
  t2                      读取 count=0
  t3   计算 0+1=1
  t4                      计算 0+1=1
  t5   写入 count=1
  t6                      写入 count=1

最终结果:count = 1(期望是 2)

结论: volatile 解决不了原子性问题,需要用 synchronizedAtomicInteger


五、总结:volatile 到底做了什么?

特性 是否保证 原理
可见性 ✅ 保证 读写直接操作主内存,缓存失效机制
禁止重排序 ✅ 保证 内存屏障阻止指令乱序
原子性 ❌ 不保证 i++ 这类复合操作仍需同步

5-1、什么时候用 volatile?

volatile 通常用于一个变量被多个线程共享,并且一个线程修改后,其他线程需要立即感知的场景。它保证变量的可见性和有序性,但不保证复合操作的原子性,所以不能替代锁

java 复制代码
// ✅ 适合用 volatile 的场景:

// 1. 状态标志位(一个线程写,其他线程读)
private volatile boolean isRunning = true;

// 2. 双重检查锁(DCL)中的单例
private volatile static Singleton instance;

// 3. 读多写少,且写入不依赖当前值
private volatile long configVersion;
1、场景一:状态标志位(最常见)
java 复制代码
public class Worker {

    private volatile boolean running = true;


    public void work(){

        while(running){

            // 执行业务
        }

        System.out.println("线程结束");
    }


    public void stop(){

        running=false;
    }
}
2、场景二:单例模式双重检查锁
java 复制代码
public class Singleton {

    private volatile static Singleton instance;


    public static Singleton getInstance(){
        // 第一重检查

        if(instance==null){

            synchronized(Singleton.class){
                // 第二重检查

                if(instance==null){

                    instance=new Singleton();

                }
            }
        }

        return instance;
    }
}

为什么需要 volatile?

重点:

java 复制代码
instance=new Singleton();

不是一步完成。


实际上:

第一步:分配对象内存:

java 复制代码
memory = new Object()

第二步:初始化对象:

java 复制代码
memory.name="xxx"

第三步:让 instance 指向对象:

java 复制代码
instance = memory

但是 JVM 可能发生指令重排序,变成:

复制代码
1. 分配内存

3. instance 指向内存

2. 初始化对象

于是:线程 A:

java 复制代码
instance != null

以为对象创建好了。

但是:对象还没有初始化完成。导致空指针。

volatile 禁止这种重排序。

3、场景三:配置刷新
java 复制代码
public class Config {

    private volatile String address;


    public void update(String newAddress){

        address=newAddress;
    }


    public String getAddress(){

        return address;
    }
}

多个线程读取配置:

java 复制代码
线程1 修改地址

线程2、3、4立即看到新地址

适合 volatile。


5-2、什么时候不用 volatile?

java 复制代码
// ❌ 不适合的场景:

// 1. 需要原子性操作的计数器
private volatile int counter;  // 错误!count++ 非原子

// 2. 多个线程同时修改同一个变量
// 应该用 AtomicInteger 或 synchronized

5-3、volatile 和 synchronized 区别

volatile synchronized
可见性
原子性
有序性
加锁
性能 相对低

volatile:

我只是告诉 JVM,这个变量变化后大家马上看到。

synchronized:

我不仅保证大家看到,还保证同一时间只有一个线程修改。


六、一句话记住 volatile

volatile 告诉 JVM:这个变量是"共享的、易变的",每次读取都要去主内存拿最新值,每次写入都要立刻刷回主内存,并且不要打乱它周围的指令顺序。

但它不保证复合操作的原子性 ------那是 synchronizedjava.util.concurrent.atomic 包该做的事。

相关推荐
lilian2332 小时前
Harmony os 技术实战|拼豆制图10:把取消、解析失败和保存失败写成可恢复状态机
java·javascript·华为·harmonyos
whn19772 小时前
达梦连接串JDBC测试程序
java·数据库
还是鼠鼠2 小时前
Spring AI ChatClient详解:创建第一个AI客户端
java·springboot·spring ai·chatclient
vHelios3 小时前
【电商项目】测试 授权功能 遇到的问题与解决方案
java·spring boot·sql
2401_894915533 小时前
GEO 定位优化源码搭建常见报错排查:数据库、伪静态、接口调试
java·数据库·网络协议·tcp/ip·spring·unity
bbq粉刷匠3 小时前
ArrayList 整体认识
java·jvm·数据结构
水无痕simon3 小时前
15 转账对接文档
java·区块链
西凉的悲伤3 小时前
Java 基于 MySQL 行锁实现高并发业务序列号生成器
java·mysql·springboot·序列号生成
lv__pf3 小时前
servlet与jsp
java·开发语言·servlet