浅谈Java中的volatile关键字

Java中的volatile关键字

volatile 是 Java 中用于修饰变量的轻量级同步关键字,核心作用是保证可见性有序性 ,但不保证原子性

1.保证可见性

  • 当一个线程修改 volatile 变量后,JVM 会强制将新值立即刷新到主内存,而非仅停留在线程本地缓存。
  • 其他线程读取该变量时,会直接从主内存加载最新值,而非使用本地缓存的旧值,避免 "缓存不一致" 导致的可见性问题。
  • 可见性的核心是「多线程环境下,一个线程对变量的修改,其他线程能立刻看到」
  • 解决了线程本地缓存和主内存的同步问题
java 复制代码
private boolean flag = false;

// 线程 A
new Thread(() -> {
    while(!flag) {
        // 一直循环,直到 flag 为 true
    }
}).start();

// 线程 B
new Thread(() -> {
    flag = true;
    System.out.println("flag 已设为 true");
}).start();

如果B修改了flag 并且写回主存 但是A可能一直在读取自己的值陷入了死循环

  • 线程 B 的修改没有被强制同步到主内存;
  • 线程 A 没有被强制去主内存读取最新值。

这就叫可见性丢失,总是一句话,你修改了某个值,立马就会和其他线程同步你修改的,防止你的数据是错误或者过时的!消除工作内存和主内存的延迟

2.禁止指令重排序

  • 编译器与 CPU 为优化性能可能对指令重排,但 volatile 通过插入内存屏障(Memory Barrier)阻止特定重排,确保代码执行顺序与预期一致。
  • volatile 的核心作用之一就是禁止指令重排序(另一个是保证可见性)

指令重排序例子(双重检查锁单例)

1. 有问题的 DCL 单例(指令重排序导致线程不安全)
java 复制代码
public class Singleton {
    // 未加 volatile,可能发生指令重排序
    private static Singleton instance;

    private Singleton() {} // 私有构造,防止外部实例化

    public static Singleton getInstance() {
        // 第一次检查:避免每次获取实例都加锁
        if (instance == null) {
            // 加锁,保证多线程下只有一个线程能进入
            synchronized (Singleton.class) {
                // 第二次检查:防止多个线程等待锁后重复创建实例
                if (instance == null) {
                    // 这里会发生指令重排序!
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

instance = new Singleton(); 这行代码看似是一步,实际 JVM 会拆分成 3 步:

  1. 分配内存空间(memory = allocate()
  2. 初始化对象(ctorInstance(memory)
  3. instance 指向分配的内存地址(instance = memory

JVM 可能对步骤 2 和 3 进行重排序,变成:

  1. 分配内存空间
  2. instance 指向内存地址(此时对象还未初始化!)
  3. 初始化对象

此时如果有另一个线程进入 getInstance() 方法,发现 instance != null,就会直接返回这个未初始化完成的对象,导致程序出错。

如何修改?

加上volatile,禁止指令重排序 private static volatile Singleton instance;

volatile 做了什么?

volatile 会在指令序列中插入内存屏障

  • 禁止写操作(步骤 3)重排序到初始化操作(步骤 2)之前;
  • 保证一个线程对 instance 的写操作,对其他线程的读操作可见。

3. 不保证原子性

  • 单个 volatile 变量的读写是原子操作,但复合操作(如 i++、i += 1)包含 "读 - 改 - 写" 三步,非原子,多线程并发仍可能出现竞态条件,需用 synchronizedAtomic 类补充。

原子性 = 不可分割性。一个操作要么完全执行完,要么完全不执行,中间不会被其他线程打断

count++count += 1count = count + 1,这些看似是一行代码

实际在底层会拆成 3 个独立步骤:

  • :从主内存读取 count 的当前值到线程工作内存;
  • :在工作内存中把 count 值 +1;
  • :把修改后的值写回主内存。

这 3 步是分开执行的,volatile 只能保证 "读的时候读最新值""写的时候立刻刷回主内存",但无法保证这 3 步作为一个整体不被打断

假设有线程 A 和线程 B 同时执行 count++,原本 count=0:

  1. 线程 A 读:从主内存拿到 count=0;
  2. 线程 B 读:也从主内存拿到 count=0(volatile 保证读的是最新值,但此时还没修改);
  3. 线程 A 改:0+1=1;
  4. 线程 B 改:0+1=1;
  5. 线程 A 写:把 1 刷回主内存;
  6. 线程 B 写:把 1 刷回主内存(覆盖了线程 A 的结果)。

原本期望 2 次 ++ 后 count=2,但实际结果是 1------ 这就是原子性丢失 ,因为 count++ 的 3 步被打断了,两个线程的修改互相覆盖。

保证原子性一般用 synchronizedAtomic 类补充

相关推荐
vx_bisheyuange2 小时前
基于SpringBoot的海鲜市场系统
java·spring boot·后端·毕业设计
移幻漂流2 小时前
C/C++并发编程详解:如何写出优秀的并发程序
c语言·开发语言·c++
康康的AI博客2 小时前
工业数据中台:PLC、SCADA、MES的实时协同架构
java·服务器·网络
余醉 | dtminer2 小时前
R语言常见新手问题
开发语言·r语言
それども2 小时前
为什么要加@ResponseBody
java·开发语言·spring boot
一只专注api接口开发的技术猿2 小时前
微服务架构下集成淘宝商品 API 的实践与思考
java·大数据·开发语言·数据库·微服务·架构
被星1砸昏头2 小时前
C++中的享元模式
开发语言·c++·算法
2501_944424122 小时前
Flutter for OpenHarmony游戏集合App实战之记忆翻牌配对消除
android·java·开发语言·javascript·windows·flutter·游戏
李慕婉学姐2 小时前
【开题答辩过程】以《基于Spring Boot和大数据的医院挂号系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
大数据·spring boot·后端