volatile简介及可见性、有序性的保证

本文介绍了Java中的volatile关键字,它提供了轻量级的同步机制。volatile保证了变量对所有线程的可见性和禁止指令重排序,从而在多线程环境下确保数据一致性。文章详细解释了volatile如何实现这两个特性,并通过代码示例和字节码分析讨论了其在有序性方面的限制。最后,文章提到了volatile在DCL单例模式中的应用及其作用。

volatile简介

volatile是jvm提供的最轻量级的同步机制(相比于synchronized,其要轻量很多)

当一个变量定义为volatile后,其具备两种特性:

  • 此变量对所有线程的可见性
    • 可见性:当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
  • 禁止指令重排序优化
    • 指令重排序:JVM为了进行优化,会对变量赋值等操作进行一系列的优化,其只保证了所有依赖赋值结果的地方都能获取到正确的结果,但不能保证该变量赋值操作的顺序与程序代码中的执行顺序一致。
    • 注意:重排序优化是机器级的优化操作,不是是Java源代码层面进行的。

可见性

普遍变量

首先,为什么普通变量不能做到可见性呢?

这里需要引入JMM(Java内存模型)。

  • 什么是JMM?

    由于在不同平台上内存模型的差异,可能同一个程序在一个平台上并发情况下可以正常运行,而在另一个平台上并发访问就出错,因此需要针对各种平台来实现一个统一的规范。由此,JVM规范了自身的JAVA内存模型(即JMM)来屏蔽操作系统的内存访问差异。

  • JMM简介

    JMM的知识点较多,这里只做简单介绍。

    首先上图
    在这里插入图片描述
    JMM规定了所有的变量都存储在主内存,而每条线程有自己的工作内存;

    线程的工作内存保存了该线程所使用到的变量的拷贝(注意:线程的工作内存只拷贝了对象的引用、和正在访问的对象中的某个字段,并不会完全拷贝此对象),线程都所有操作都是在自己的工作内存中进行的;

    (注意:JMM与JVM中内存区域的堆栈等区域不是一个层次的内存划分,读者不要混淆)

okay,引入完毕,回到刚才的问题,为什么普通变量不能做到可见性呢?

由上图及介绍可以知道,普通变量的值传递需要通过主内存来完成,例如:线程A修改一个变量的值,需要先向主内存回写后,另一个线程B等到A回写完成后再读取,才能够读取到变量的新值。

volatile修饰的变量

volatile怎样实现可见的呢?

有如下java代码

public class Test16 {
   
   
    private volatile int a=0;
    public void update() {
   
   
        a = 1;
    }
    public static void main(String[] args) {
   
   
    }
}

通过hsdis+jitwatch工具查看其汇编码(查看步骤见:here),如下:

......
  0x000000000295156d: lock addl %rdi,(
<think>我们要讨论volatile关键字如何保证可见性有序性。根据提供的引用资料,我们可以总结如下: 1. **可见性**:volatile修饰的变量在被一个线程修改后,会立即将修改后的值刷新到主内存,并导致其他线程的工作内存中该变量的副本失效,从而强制其他线程从主内存重新读取该变量的最新值[^3][^2][^4]。 2. **有序性**:volatile通过禁止指令重排序来保证有序性。具体来说,volatile会在生成字节码时插入内存屏障(Memory Barrier),防止编译器和处理器对指令进行重排序优化,从而确保多线程环境下程序执行的顺序性[^2][^5]。 现在,我们将按照要求详细解释这两个原理,并使用适当的LaTeX格式。 ### 可见性原理 在Java内存模型(JMM)中,每个线程都有自己的工作内存(工作内存是线程私有的,可以理解为CPU缓存或寄存器),线程对变量的操作都是先在工作内存中进行的,然后再同步到主内存。volatile关键字通过以下机制保证可见性: 1. **立即刷新**:当对volatile变量进行写操作时,写入后立即将新值同步到主内存[^3]。 2. **失效机制**:当其他线程读取volatile变量时,会检查工作内存中的值是否已经失效(根据缓存一致性协议如MESI协议),如果失效则从主内存重新加载[^3][^4]。 具体实现依赖于**缓存一致性协议**(如MESI协议)和**总线嗅探机制**: - 当某个线程修改了volatile变量,该修改会通过总线传播到主内存。 - 其他处理器通过总线嗅探机制,监听到数据变化,并将自己工作内存中的缓存行置为无效状态(Invalid)[^3][^5]。 ### 有序性原理 有序性指的是在多线程环境下,程序执行的顺序按照代码的顺序执行。但编译器和处理器可能会进行指令重排序优化以提高性能。volatile通过插入**内存屏障**来禁止重排序,保证有序性。 JMM定义了四种内存屏障: - **LoadLoad屏障**:确保该屏障之前的读操作先于之后的读操作完成。 - **StoreStore屏障**:确保该屏障之前的写操作先于之后的写操作完成。 - **LoadStore屏障**:确保该屏障之前的读操作先于之后的写操作完成。 - **StoreLoad屏障**:确保该屏障之前的写操作先于之后的读操作完成(全能型屏障,开销最大)[^4][^5]。 对于volatile变量: - **写操作**:在写操作前插入StoreStore屏障,写操作后插入StoreLoad屏障。 - **读操作**:在读操作后插入LoadLoad和LoadStore屏障。 #### 写操作屏障示例: ```assembly StoreStore屏障 volatile写操作 StoreLoad屏障 ``` #### 读操作屏障示例: ```assembly volatile读操作 LoadLoad屏障 LoadStore屏障 ``` 这些屏障限制了指令重排序,保证了以下顺序: 1. 在写volatile变量时,该变量之前的所有操作(读写)都不能重排序到写操作之后。 2. 在读volatile变量时,该变量之后的所有操作(读写)都不能重排序到读操作之前。 ### 原子性问题 虽然volatile保证可见性有序性,但它不保证操作的原子性。例如,多线程下对volatile变量的复合操作(如自增)仍然是非原子性的,需要配合synchronized或原子类(如AtomicInteger)使用[^2]。 ### 应用示例 引用[5]给出了一个使用volatile防止指令重排序的示例: ```java int a = 0; volatile boolean flag = false; int b = 0; // 线程A执行 a = 1; // Step 1 flag = true; // Step 2,volatile写,确保Step1不会被重排序到Step2之后(同时Step1也不会被重排序到前面任意操作之后?实际上,volatile写之前的任何操作不能重排到它后面,因此Step1和Step2的顺序不会交换) b = 2; // Step 3(注意:这里Step3不是volatile写,所以可以与前面重排序,但由于屏障的存在,Step2和Step3之间是StoreStore屏障,所以Step3不会重排序到Step2之前。同时,Step3不会重排序到volatile写之后?不对,因为Step3在volatile写之后,没有屏障保护它和后面的操作。但是Step2后面的StoreLoad屏障会保证Step2写操作完成后,后面的操作(Step3)不会被提前到volatile写之前。所以实际上Step3不会重排到Step2之前,但Step3可能会被重排到Step1之前吗?不行,因为Step1和Step2之间有一个隐式的StoreStore屏障,保证Step1在Step2之前。所以三个步骤的顺序都是固定的?) // 线程B执行 if (flag) { // 确保先看到a=1,再看到b=2?实际上,由于volatile读操作的内存屏障,线程B在读取到flag=true后,它前面的所有写操作(包括a=1)都已经完成并刷新到主存,所以此时a一定为1。但b=2不一定,因为b=2是在volatile写之后,线程B在读取flag之后,还需要读取b,而b=2可能还没被线程B看到。所以这里线程B只能确保a=1已经可见,而b可能为0或2(因为非volatile变量b的可见性不能保证)。 } ``` 注意:上例中,volatile变量flag的写操作保证了Step1(a=1)不会被重排序到Step2之后,同时Step2之前的写操作(包括Step1)都会在volatile写之前刷新到主存。但Step3(b=2)是非volatile的,它的写入对其他线程的可见性无法保证。 ### 总结:volatile的关键原理 | 特性 | 保障机制 | |--------|--------------------------------------------------| | 可见性 | 写操作强制刷新主内存;读操作强制从主内存重新加载 | | 有序性 | 通过内存屏障禁止指令重排序(写前StoreStore,写后StoreLoad;读后LoadLoad+LoadStore) | ### 相关问题 1. volatile如何防止指令重排序?它和synchronized在有序性保证上的区别是什么? 2. 为什么volatile变量不能保证原子性操作?请举例说明。 3. 在DCL(双重检查锁定)单例模式中,volatile关键字如何避免由于指令重排序导致的异常? 4. Java内存模型(JMM)中除了volatile,还有哪些机制可以保证可见性? </think> ### volatile关键字保证可见性有序性原理 #### 🧠 **1. 可见性原理** **问题根源**: 在Java内存模型(JMM)中,线程操作共享变量时存在「主内存→工作内存」的复制机制(如图),导致线程间数据不一致: ```mermaid graph LR A[主内存] -->|复制变量| B[线程1工作内存] A -->|复制变量| C[线程2工作内存] B -->|修改后可能延迟写入| A C -->|修改后可能延迟写入| A ``` **volatile解决方案**: 1. **强制同步写操作** 当写`volatile`变量时: - 立即将工作内存中的修改**刷新到主内存**(`StoreBarrier`)[^3][^4] - 触发**总线嗅探机制**通知其他CPU核心 2. **强制同步读操作** 当读`volatile`变量时: - 使其他线程工作内存中该变量的副本**失效**(MESI协议) - **强制从主内存重新加载**最新值(`LoadBarrier`)[^3] > 📌 **本质**:通过**内存屏障 + 缓存一致性协议**确保变量修改全局可见[^2][^5]。 --- #### ⚙️ **2. 有序性原理** **问题根源**: 编译器和处理器可能对指令进行**重排序优化**,破坏代码执行顺序: ```java // 未加volatile时可能的乱序执行 a = 1; // 普通写操作 flag = true; // 可能被重排到a=1之前 ``` **volatile解决方案**: 通过插入**内存屏障**禁止特定重排序: | 屏障类型 | 作用 | 对应volatile操作 | |----------------|----------------------------------------------------------------------|-------------------------| | `StoreStore` | 禁止上方普通写与下方`volatile写`重排序 | 写操作前插入[^5] | | `StoreLoad` | 禁止`volatile写`与下方可能的`volatile读/写`重排序 | 写操作后插入 | | `LoadLoad` | 禁止下方普通读与`volatile读`重排序 | 读操作后插入[^4] | | `LoadStore` | 禁止`volatile读`与下方普通写重排序 | 读操作后插入 | **屏障插入规则**: ```java // volatile写操作 [普通写指令] StoreStore屏障 // 1. 确保上方所有写完成 [volatile写指令] StoreLoad屏障 // 2. 确保volatile写全局可见 // volatile读操作 [volatile读指令] LoadLoad屏障 // 1. 禁止与下方读重排序 LoadStore屏障 // 2. 禁止与下方写重排序 [普通读/写指令] ``` > 📌 **关键效果**: > - volatile写前的操作**绝不会重排到写之后**(保证写前操作全局可见) > - volatile读后的操作**绝不会重排到读之前**(保证读到最新值后操作)[^4][^5] --- #### 🔬 **实验验证** 通过双重检查锁单例模式验证有序性: ```java public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized(Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // volatile禁止重排序 } } } return instance; } } ``` 若未用`volatile`: ```java instance = new Singleton(); // 可能被拆分为: 1. 分配内存空间 2. 对象初始化 ← 重排序到这 3. instance指向内存地址 ← 第2步被重排到此 ``` 其他线程可能在对象未完成初始化时访问到非空`instance`。 **volatile通过`StoreStore`屏障**确保步骤`3`不会重排到步骤`2`之前[^5]。 --- #### ⚠️ **原子性局限** 尽管保证可见性有序性,**volatile保证原子性**(复合操作仍需锁或原子类): ```java volatile int count = 0; count++; // 实际包含三步:读值→+1→写回(非原子) ``` --- ### 📚 核心原理对比 | 特性 | 实现机制 | 技术支撑 | |-----------|----------------------------------|------------------------------| | **可见性** | 写刷新主存 + 读强制重载 | 内存屏障 + 缓存一致性协议 | | **有序性** | 插入四大内存屏障 | StoreStore/LoadLoad等屏障 | --- ### ❓ 相关问题 1. **`volatile`与`synchronized`在有序性保证上有什么区别?**(分析两种方法的屏障差异) 2. **`volatile`变量为何不能代替锁实现i++的线程安全?**(结合可见性与原子性矛盾解释) 3. **Java内存模型如何用happen-before规则解释`volatile`的可见性?** 4. **在ARM等弱内存模型架构中,`volatile`的实现有何不同?**
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值