浅谈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)包含 "读 - 改 - 写" 三步,非原子,多线程并发仍可能出现竞态条件,需用 synchronized 或 Atomic 类补充。

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

像count++、count += 1、count = 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 步被打断了,两个线程的修改互相覆盖。

保证原子性一般用 synchronized 或 Atomic 类补充

相关推荐
海鸥-w1 小时前
什么是AOP
java
java资料站1 小时前
十一、Spring AI Alibaba · 高级 · 多智能体(Multi-agent)
人工智能·spring·microsoft
Cc.Y1 小时前
Java零基础入门:可变字符串与包装类:StringBuilder、StringBuffer 与 通讯录管理系统实战
java·开发语言·python
paopaokaka_luck1 小时前
基于springboot+vue的智慧家政管理系统(AI问答、 WebSocket实时沟通、ECharts运营分析、订单规则与服务追踪)
java·javascript·spring boot·数据分析·echarts·mybatis
sm_926787051 小时前
RFID 标签打印的技术实现要点与二次开发实践
java·大数据·前端·c++·编辑器
阿森_dev1 小时前
2027 届毕设选题怎么选:30 个 Java 方向选题的实现难度排序
java·开发语言·spring boot·毕业设计·课程设计
by209991 小时前
C++ 多态:同一个基类接口,调用不同的函数
开发语言·c++·经验分享·多态
笔墨登场说说2 小时前
flink bin/start-cluster.sh 帮我做成开机启动
开发语言·python
Sarvartha2 小时前
Lambda 表达式和函数式接口
java·开发语言
计算机毕业设计杰瑞2 小时前
【计算机毕设精品】基于SpringBoot的一体化教务与学业质量管理系统的设计与实现,附源码_增删改查_java项目_可完美运行部署_网站项目
java·spring boot·课程设计