【Java EE】volatile关键字
2026/4/24 11:36:16 网站建设 项目流程

volatile关键字

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

解决可见性问题

先看一段代码:

publicclassVisibilityDemo{privatestaticbooleanflag=false;publicstaticvoidmain(String[]args)throwsInterruptedException{//读线程newThread(()->{System.out.println("线程A:等待flag变为true");while(!flag){// 忙等待}System.out.println("线程A:检测到flag为true,退出");}).start();Thread.sleep(1000);//写线程newThread(()->{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):

publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){// 第一次检查synchronized(Singleton.class){if(instance==null){// 第二次检查instance=newSingleton();}}}returninstance;}}

这段代码看似完美,实际上存在隐患。问题出在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 解决了可见性和有序性,但有一个重要的限制需要牢记:它不保证原子性

publicclassAtomicityDemo{privatestaticvolatileintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{CountDownLatchlatch=newCountDownLatch(10);for(inti=0;i<10;i++){newThread(()->{for(intj=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 的区别?

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

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++)

volatileintcount=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 规则

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询