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

相关推荐
蜡笔小马7 分钟前
10.Boost.Geometry R-tree 空间索引详解
开发语言·c++·算法·r-tree
IOsetting8 分钟前
金山云主机添加开机路由
运维·服务器·开发语言·网络·php
kali-Myon8 分钟前
2025春秋杯网络安全联赛冬季赛-day1
java·sql·安全·web安全·ai·php·web
我是咸鱼不闲呀12 分钟前
力扣Hot100系列20(Java)——[动态规划]总结(下)( 单词拆分,最大递增子序列,乘积最大子数组 ,分割等和子集,最长有效括号)
java·leetcode·动态规划
清水白石00821 分钟前
深入解析 LRU 缓存:从 `@lru_cache` 到手动实现的完整指南
java·python·spring·缓存
林开落L22 分钟前
从零开始学习Protobuf(C++实战版)
开发语言·c++·学习·protobuffer·结构化数据序列化机制
牛奔26 分钟前
Go 是如何做抢占式调度的?
开发语言·后端·golang
符哥200834 分钟前
C++ 进阶知识点整理
java·开发语言·jvm
小猪咪piggy35 分钟前
【Python】(4) 列表和元组
开发语言·python
Sayuanni%31 小时前
初阶_多线程1(线程含义与关键属性)
java