浅谈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 类补充

相关推荐
侠客行03175 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪5 小时前
深入浅出LangChain4J
java·langchain·llm
灰子学技术7 小时前
go response.Body.close()导致连接异常处理
开发语言·后端·golang
老毛肚7 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎7 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
二十雨辰7 小时前
[python]-AI大模型
开发语言·人工智能·python
Yvonne爱编码8 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚8 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂8 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
pas1368 小时前
41-parse的实现原理&有限状态机
开发语言·前端·javascript