【Java EE】volatile关键字

volatile关键字

  • 解决可见性问题
  • 解决指令重排序
  • 不保证原子性
  • volatile关键字底层原理------内存屏障
  • 面试题
    • [volatile 关键字的作用是什么?](#volatile 关键字的作用是什么?)
    • [volatile 和 synchronized 的区别?](#volatile 和 synchronized 的区别?)
    • [volatile 能替代锁吗?为什么?](#volatile 能替代锁吗?为什么?)
    • [volatile 底层是怎么实现的?](#volatile 底层是怎么实现的?)
    • 什么是内存屏障?有哪些类型?
    • [DCL 单例为什么要加 volatile?](#DCL 单例为什么要加 volatile?)
    • [volatile 变量写操作后,其他线程为什么能看到?](#volatile 变量写操作后,其他线程为什么能看到?)
    • 这段代码为什么会有问题?(count++)
    • [Java 内存模型(JMM)中的 8 个原子操作和 volatile 的关系?](#Java 内存模型(JMM)中的 8 个原子操作和 volatile 的关系?)
    • [volatile 和 final 在内存语义上有什么区别?](#volatile 和 final 在内存语义上有什么区别?)
    • [除了 volatile,还有什么方式能保证可见性?](#除了 volatile,还有什么方式能保证可见性?)

解决可见性问题

先看一段代码:

java 复制代码
public class VisibilityDemo {
    private static boolean flag = false;
    
    public static void main(String[] args) throws InterruptedException {
    	//读线程
        new Thread(() -> {
            System.out.println("线程A:等待flag变为true");
            while (!flag) {
                // 忙等待
            }
            System.out.println("线程A:检测到flag为true,退出");
        }).start();
        
        Thread.sleep(1000);
        //写线程
        new Thread(() -> {
            flag = true;
            System.out.println("线程B:已将flag设为true");
        }).start();
    }
}

结果分析:线程B将 flag 改为 true 后,线程A应该立即跳出循环。但实际运行结果往往是:线程A陷入了死循环,永远无法退出

为什么会这样?这涉及到并发编程中第一个核心问题------可见性

在 Java 内存模型(JMM)中,每个线程都有自己的工作内存(CPU 缓存),共享变量存储在主内存中。线程B修改了 flag,只是更新了自己工作内存中的副本,还没来得及同步到主内存;而线程A读取 flag 时,也只会从自己的工作内存中读取。两个线程各自维护着一份副本,自然看不到对方的变化。

volatile 的第一个作用就是解决这个问题 :将 flag 声明为 volatile boolean flag = false; 后,任何线程对它的修改都会立即刷新到主内存,任何线程读取它时也会直接从主内存获取最新值,从而保证可见性

解决指令重排序

解决了可见性问题,还有第二个陷阱。来看单例模式中的经典实现------双重检查锁定(Double-Checked Locking):

java 复制代码
public class Singleton {
    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() 这一行,它不是原子操作,而是分为多个步骤:

  1. 分配内存空间
  2. 初始化对象(执行构造函数)
  3. 将 instance 引用指向分配的内存空间

为了提高执行效率,编译器和处理器可能会对指令进行重排序 ,将步骤3提前到步骤2之前。此时如果另一个线程恰好在 instance 不为 null 但对象还未初始化完成时调用 getInstance(),就会拿到一个"半成品"对象,可能导致程序崩溃。

volatile 的第二个作用就是禁止指令重排序 :将 instance 声明为 private static volatile Singleton instance; 后,JVM 会在 volatile 写操作前后插入内存屏障,确保 instance = new Singleton() 的执行顺序不会被重排,从而避免对象逸出问题。

不保证原子性

volatile 解决了可见性和有序性,但有一个重要的限制需要牢记:它不保证原子性

java 复制代码
public class AtomicityDemo {
    private static volatile int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    count++;  // 复合操作,非原子性
                }
                latch.countDown();
            }).start();
        }
        latch.await();
        System.out.println("期望值:10000,实际值:" + count);
    }
}

多次运行这段代码,你会发现 count 的值总是小于 10000。原因在于 count++ 虽然看起来是一行代码,实际上包含三个步骤:读取值 → 加1 → 写回。volatile 只能保证每次读取时拿到最新的值,但无法防止多个线程交错执行这些步骤导致的丢失更新问题。

如果需要原子性操作,应该使用 AtomicIntegersynchronized

volatile关键字底层原理------内存屏障

volatile 的可见性和有序性,底层都是通过内存屏障(Memory Barrier)实现的。

内存屏障是一种 CPU 指令,分为四种类型:

屏障类型 作用
LoadLoad 读-读屏障,后面的读不能重排到前面的读之前
StoreStore 写-写屏障,后面的写不能重排到前面的写之前
LoadStore 读-写屏障,后面的写不能重排到前面的读之前
StoreLoad 写-读屏障,后面的读不能重排到前面的写之前(全能型,开销最大) (同时刷新写缓冲区)

对于 volatile 变量的操作,JVM 会按以下策略插入内存屏障:

  • volatile 写之前:插入 StoreStore 屏障,确保之前的普通写操作已完成
  • volatile 写之后:插入 StoreLoad 屏障,强制将新值刷新到主内存
  • volatile 读之后:插入 LoadLoad 和 LoadStore 屏障,禁止后续读写被重排到前面

这些屏障在底层会被优化。x86 本身就是强内存模型,它保证了 LoadLoadLoadStoreStoreStore 顺序,所以其实在 x86 上,volatile 的底层实现通常只需要一个 lock addl(对应 StoreLoad)就足够了。

面试题

volatile 关键字的作用是什么?

  • 保证可见性:一个线程修改 volatile 变量后,其他线程能立即看到最新值
  • 禁止指令重排序:通过内存屏障确保有序性
  • 不保证原子性 :复合操作(如 count++)仍有并发风险

volatile 和 synchronized 的区别?

维度 volatile synchronized
可见性
原子性
线程阻塞 不会
性能开销 高(有锁竞争)
使用位置 变量修饰 方法/代码块

volatile 能替代锁吗?为什么?

  • 不能,因为 volatile 只保证可见性和有序性
  • 对于复合操作(读-改-写)无能为力,必须用锁或 CAS
  • 举例:count++ 用 volatile 依然会丢更新

volatile 底层是怎么实现的?

  • JVM 层面:通过内存屏障(Memory Barrier)实现
  • 对 volatile 变量的写操作,会插入 StoreStoreStoreLoad 屏障
  • 对 volatile 变量的读操作,会插入 LoadLoadLoadStore 屏障
  • 硬件层面(x86):通过 lock 前缀指令实现(如 lock addl
  • lock 会锁定总线或缓存行,强制将修改刷新到主存,并使其他 CPU 缓存行失效

什么是内存屏障?有哪些类型?

屏障类型 作用
LoadLoad 读-读屏障,后面的读不能重排到前面的读之前
StoreStore 写-写屏障,后面的写不能重排到前面的写之前
LoadStore 读-写屏障,后面的写不能重排到前面的读之前
StoreLoad 写-读屏障,后面的读不能重排到前面的写之前(全能型,开销最大)

DCL 单例为什么要加 volatile?

  • instance = new Singleton() 不是原子操作,分三步:
    1. 分配内存
    2. 初始化对象(执行构造方法)
    3. 将引用指向内存
  • 步骤 2 和 3 可能被重排,导致对象"半初始化"时引用就不为 null
  • 其他线程拿到这个"半成品"对象,可能出问题(字段为默认值)
  • volatile 禁止了对象创建过程的重排序

volatile 变量写操作后,其他线程为什么能看到?

  • JMM 规范:volatile 写会将当前工作内存的值强制刷新到主内存
  • 同时会使其他 CPU 中该变量的缓存行失效(MESI 协议的 Invalidate 机制)
  • 其他线程再读时,必须从主内存重新加载
  • 这是 volatile 可见性的完整闭环

JMM

这段代码为什么会有问题?(count++)

java 复制代码
volatile int count = 0;

// 10个线程各加1000次
count++;
  • count++ 是复合操作:读 → 加1 → 写
  • volatile 只保证读和写的可见性,但两次操作之间可能被其他线程打断
  • 最终结果会小于 10000
  • 解决方案:使用 AtomicIntegersynchronized

Java 内存模型(JMM)中的 8 个原子操作和 volatile 的关系?

  • 8 个操作:lock、unlock、read、load、use、assign、store、write
  • volatile 规定:对变量的 read-load-use 必须连续;assign-store-write 也必须连续
  • 本质上是约束了这些原子操作的执行顺序,不允许中间插入其他操作

volatile 和 final 在内存语义上有什么区别?

  • final 用于保证初始化安全性:构造函数执行完后,final 字段对任意线程可见
  • volatile 保证的是每次读写的可见性
  • 一个对象的所有 final 字段,在构造函数结束时会有一个"冻结"操作(StoreStore 屏障)
  • final 的优势:初始化后不需要同步开销,适合不可变对象
    final有点像string,但是string本质不是final实现

除了 volatile,还有什么方式能保证可见性?

  • synchronized:锁释放前会将工作内存刷新到主存
  • Lock:与 synchronized 类似
  • AtomicInteger 等原子类:底层用 volatile 修饰 value 字段
  • final:初始化安全性保证构造结束后的可见性
  • Thread.join() / Thread.start():JMM 保证的 happens-before 规则
相关推荐
A_aspectJ2 小时前
Java开发的学习优势:稳定基石与多元可能并存的技术赛道
java·开发语言
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【36】FlowAgent 和 BaseAgent 抽象类
java·人工智能·spring
qq_283720052 小时前
Python 模块精讲:collections —— 高级数据结构深度解析(defaultdict、Counter、deque)
java·开发语言
乐嘉明2 小时前
在线堆文件分析功能
java·ai
青槿吖2 小时前
第二篇:从复制粘贴到自定义规则!Spring Cloud Gateway 断言 + 过滤全玩法,拿捏微服务流量管控
java·spring boot·后端·spring cloud·微服务·云原生·架构
SamDeepThinking2 小时前
C端多渠道用户体系设计:从需求到落地
java·后端·架构
天若有情6732 小时前
反向封神!C++ 全局单例不避反用,实现无锁多线程函数独占访问
java·javascript·c++
凤凰院凶涛QAQ3 小时前
《C++转JAVA快速入手系列》:基本通用语法篇
java·开发语言·c++
千寻girling3 小时前
机器学习 | 逻辑回归 | 尚硅谷学习
java·人工智能·python·学习·算法·机器学习·逻辑回归