volatile特性详解

目录

1 前言

2 volatile变量读写的原子性和普通变量读写的非原子性

2.1 volatile读写原子性依据

2.2 普通变量读写非原子性依据

2.3 非原子性例子

3 可见性

4 禁止指令重排序


1 前言

volatile变量有三个特性:

  • 原子性:对任意单个volatile变量的读/写具有原子性。
  • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入(靠原子性保障)。
  • 能够防止相关指令重排序。

2 volatile变量读写的原子性和普通变量读写的非原子性

volatile变量的读和写操作都是原子的,在读和写的过程中其它线程不能对变量进行读或写。而普通变量是没办法做到的,因为将普通变量读和写并不是原子的,非原子操作的各条指令之间Anything can happen。

2.1 volatile读写原子性依据

根据《java并发编程的艺术》,可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。示例代码如下:

class VolatileFeaturesExample {
    //使用volatile声明64位的long型变量
    volatile long vl = 0L;

    public void set(long l) {
        vl = l;   //单个volatile变量的写
    }
    public void getAndIncrement () {
        vl++;    //复合(多个)volatile变量的读/写
    }
    public long get() {
        return vl;   //单个volatile变量的读
    }
}

上面的代码可以等价于:

class VolatileFeaturesExample {
    long vl = 0L;               // 64位的long型普通变量

    //对单个的普通 变量的写用同一个锁同步
    public synchronized void set(long l) {             
       vl = l;
    }

    public void getAndIncrement () { //普通方法调用
        long temp = get();           //调用已同步的读方法
        temp += 1L;                  //普通写操作
        set(temp);                   //调用已同步的写方法
    }
    public synchronized long get() { 
        //对单个的普通变量的读用同一个锁同步
        return vl;
    }
}

其揭示了volatile变量的一个重要特性:volatile的读写操作可以看成是原子的。

2.2 普通变量读写非原子性依据

在java内存模型中,普通变量的读写操作是不具有原子性的,这里我们必须要提到java内存模型的内存交互操作。《深入理解java虚拟机》书中介绍了在java内存模型中,主内存与工作内存之间的具体交互是通过8种操作来完成的,虚拟机保证了这八种操作每一种都是原子的,8中操作如下:

lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

书中还介绍了:如果要把一个变量从主内存复制到工作内存,就要顺序地执行read和load操作(该过程对应全局变量的读操作);如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作(该过程对应全局变量的写操作)同时,java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行。也就是说,read与load之间、store与write之间是可以插入其它指令的(包括线程的切换),这就是为什么说普通变量的读写操作不具有原子性。

2.3 非原子性例子

如果有两个线程对主内存中的普通变量a进行访问,线程1先对a进行写操作,线程2随后对a进行读操作,则可能出现的顺序是store a(线程1)、read a(线程2)、load a(线程2)、write a(线程1),这个代码执行的结果是线程2读的仍是线程1修改之前的值,而从代码顺序上来看,我们的预期是线程2读到的是线程1修改之后的值,这就导致对变量a的修改不能立刻被其它线程所知道。

从上述过程我们可以得出一个结论,那就是对于一个普通的全局变量来说,组成读写操作的每一条指令是原子的,但读或写操作并不仅仅是由一条指令组成,且jvm中允许在一个操作的多条指令中插入其它指令,这就导致了对普通变量的写操作可能对其它线程不是立即可见的,对某个变量的读操作可能会出现读到过期数据(读期间被修改)的情况。

3 从JMM(Java内存模型)角度看立即可见性

volatile变量的可见性体现在两方面

  • 对一个volatile变量的读总是能看到任意线程对这个变量最后的写,即每次读取volatile变量的时候,会使线程工作内存中的变量副本无效,从而必须从主内存中更新变量值。
  • 对一个volatile变量进行写操作后,会立刻将修改的值同步到主内存,以便于后面读取该变量的线程获取到的是最新的值。

通过第2点可知,volatile变量克服了普通变量读写操作的非原子性弊端,在写回主内存的过程中其它线程不能对变量进行读或写,直到当前线程把变量从主内存读到工作内存或写到主内存为止。通过将读写操作变成原子操作,可以保证:线程1对变量a的写操作若先发生于线程2对变量a的读操作,那么线程2读到的a的值必定是线程1修改过的最新值。

3.1 从操作系统角度看立即可见性

对volatile变量进行的写操作代码,翻译成汇编语言后会发现在写指令的后面会多出另一行汇编代码。lock前缀指令的作用是:

  1. 将当前处理器缓存行中的数据写入到内存中。
  2. 使该数据在其它处理器中的缓存失效。

这样下次其它处理器读取该volatile变量数据时都会从内存中获取,不会直接从缓存中获取。

volatile Object instance = new Singleton();

// 汇编指令
movb addr1, addr2;
lock addl addr1,addr2;    // 多出的汇编代码

4 禁止指令重排序

在jvm中,指令重排序在多个线程同时访问一个普通变量的情况下,可能会导致程序执行结果发生错误,具体例子可以查看我的另一篇文章:指令重排序。为了避免这种错误,可以用volatile变量代替普通变量。

为了实现volatile语义,java内存模型(JMM)会分别限制这两种类型的重排序类型,具体见下表:

第一个操作(行) | 是否能重排序 | 第二个操作(列)普通读/写volatile读volatile写
普通读/写  NO
volatile读NONONO
volatile写 NONO

比如说,当第一个操作是对普通变量的读/写,第二个操作是对volatile变量的写时,不允许第一个操作重排序到第二个操作后面。

为了限制重排序,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,内存屏障的具体知识参见:指令重排序。对于一个编译器来说,发现一个最优不止来最小化插入屏障的总数几乎不可能,为此java内存模型采取保守策略:

  • 每个volatile写操作的前面插入一个StoreStore屏障。
  • 每个volatile写操作的后面插入一个StoreLoad屏障。
  • 每个volatile读操作的后面插入一个LoadLoad屏障。
  • 每个volatile读操作的后面插入一个LoadStore屏障。

上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确得volatile内存语义。

等等,这里好像有个问题,表中显示,当第一个操作时普通读、第二个操作时volatile写时,重排序不被允许。但volatile的4个内存屏障插入策略似乎不能防止这种情况,因为volatile写操作之前没有插入LoadStore屏障?这个问题留着之后去探究吧。

5 volatile可见性底层实现(缓存一致性MESI)

https://www.sohu.com/a/305919295_458015

`volatile` 是 Java 和 C/C++ 等语言中用于修饰变量的关键字,主要作用是**禁止编译器和处理器对变量的访问进行优化**,确保每次访问都直接从内存中读取或写入,而非使用寄存器或缓存中的副本。以下是关键点解析: --- ### **核心作用** 1. **可见性保证** - 修改后的值会立即对其他线程可见(避免因缓存不一致导致的数据滞后)。 - 适用于多线程环境下共享变量的场景(如状态标志、中断信号)。 2. **禁止指令重排序** - 防止编译器或 CPU 对 `volatile` 变量的读写操作进行乱序优化(保障程序执行顺序)。 3. **不保证原子性** - `volatile` 仅保证单次读/写的原子性(如 `long`/`double` 在 32 位系统中可能需额外处理)。 - **复合操作**(如 `i++`)仍需同步机制(如 `synchronized` 或 `Lock`)。 --- ### **典型应用场景** 1. **状态标志** ```java volatile boolean running = true; void stop() { running = false; } // 其他线程能立即看到修改 ``` 2. **双重检查锁定(DCL)优化** ```java class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } ``` - `volatile` 防止指令重排序导致未初始化对象被访问。 3. **中断信号** - 在 I/O 操作或线程中断中,通过 `volatile` 变量通知其他线程停止等待。 --- ### **与 `synchronized` 的区别** | 特性 | `volatile` | `synchronized` | |--------------------|--------------------------------|-------------------------------| | **原子性** | 仅保证单次读/写 | 保证复合操作(如 `i++`)的原子性 | | **可见性** | 立即对其他线程可见 | 退出同步块时刷新变量到主内存 | | **有序性** | 禁止重排序 | 禁止重排序 | | **性能开销** | 极低(无锁) | 较高(涉及锁竞争) | --- ### **注意事项** 1. **非线程安全替代品** - `volatile` 不能替代 `synchronized` 或 `Lock` 处理复合操作。 2. **64 位变量(Java)** - 在 32 位 JVM 中,`long`/`double` 的读写可能非原子性,需用 `volatile` 保证。 3. **C/C++ 中的差异** - C/C++ 的 `volatile` 仅禁止编译器优化,不保证跨线程可见性(需结合内存屏障或原子操作)。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值