我们提供安全,免费的手游软件下载!

安卓手机游戏下载_安卓手机软件下载_安卓手机应用免费下载-先锋下载

当前位置: 主页 > 软件教程 > 软件教程

volatile:可见性、有序性与原子性

来源:网络 更新时间:2024-09-29 09:31:02

volatile是一种轻量级的同步机制,用于解决可见性和有序性问题,但并不保证原子性。

volatile的作用包括:

  1. 保证不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,其他线程能够立即看到这个新值。
  2. 禁止进行指令重排序。

volatile的底层原理

内存屏障

volatile通过内存屏障来维护可见性和有序性。硬件层的内存屏障主要分为两种:Load Barrier(读屏障)和 Store Barrier(写屏障)。在Java内存屏障中,涉及四种屏障的排列组合。

  1. 每个volatile写前插入StoreStore屏障,用于禁止之前的普通写和volatile写的重排序,并保证可见性。
  2. 每个volatile写后插入StoreLoad屏障,防止volatile写与之后可能有的volatile读/写重排序。
  3. 每个volatile读后插入LoadLoad屏障,禁止之后所有的普通读操作和volatile读操作重排序。
  4. 每个volatile读后插入LoadStore屏障,禁止之后所有的普通写操作和volatile读重排序。

插入内存屏障相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个volatile字段进行写操作时,Java内存模型会在写操作后插入一个写屏障指令,将之前的写入值都刷新到内存。

可见性原理

当对volatile变量进行写操作时,JVM会向处理器发送一条Lock#前缀的指令,实现两个步骤:

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 将其他处理器中缓存了该数据的缓存行设置为无效。

这是因为缓存一致性协议,每个处理器通过总线嗅探和MESI协议来检查自己的缓存是否过期。当处理器发现自己的缓存行对应的内存地址被修改,会将当前处理器的缓存行置为无效状态。处理器进行修改操作时,会重新从系统内存中将数据读取到处理器缓存中。

缓存一致性协议:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态。当其他CPU需要读取这个变量时,就会从内存重新读取。

因此,volatile写是先插入内存屏障,然后再更新对应的主存地址的数据。

有序性原理

volatile的happens-before关系

根据happens-before规则中的volatile变量规则:对一个volatile域的写操作happens-before于任意后续对这个volatile域的读操作。

举例:

//假设线程A执行writer方法,线程B执行reader方法
class VolatileExample {
    int a = 0;
    volatile boolean flag = false;
    
    public void writer() {
        a = 1;              // 1 线程A修改共享变量
        flag = true;        // 2 线程A写volatile变量
    } 
    
    public void reader() {
        if (flag) {         // 3 线程B读同一个volatile变量
        int i = a;          // 4 线程B读共享变量
        ……
        }
    }
}

根据此规则,程序建立了三类happens-before关系:

  • 根据程序次序规则:1 happens-before 2 且 3 happens-before 4。
  • 根据volatile规则:2 happens-before 3。
  • 根据happens-before的传递性规则:1 happens-before 4。

因此,线程A将volatile变量flag更改为true后,线程B能够迅速感知。

volatile禁止重排序

为了性能优化,JMM在不改变正确语义的前提下,允许编译器和处理器对指令序列进行重排序。JMM提供了内存屏障来阻止这种重排序。

JMM会针对编译器制定volatile重排序规则表。

为了实现volatile内存语义,编译器在生成字节码时会在指令序列中插入内存屏障,针对编译器来说,JMM采取了保守的策略:

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

volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。

为什么不能保证原子性

在多线程环境中,原子性指一个操作或一系列操作要么完全执行,要么完全不执行,不会被其他线程的操作打断。

尽管volatile关键字可以确保一个线程对变量的修改对其他线程立即可见,但对于读-改-写的操作序列来说是不够的。这是因为这些操作序列本身并不是原子的,例如:

public class Counter {
    private volatile int count = 0;
    
    public void increment() {
        count++; // 这实际上是三个独立的操作:读取count的值,增加1,写回新值到count
    }
}

在这个例子中,尽管count变量被声明为volatile,但increment()方法并不是线程安全的。多个线程同时调用increment()可能导致count的值只增加了1,而不是期望的2,因为count++操作不是原子的。

为了保证原子性,可以使用synchronized关键字或者java.util.concurrent.atomic包中的原子类(如AtomicInteger),这些机制能够保证此类操作的原子性。

关于作者

来自一线程序员Seven的探索与实践,持续学习迭代中~

本文已收录于我的个人博客: https://www.seven97.top

公众号:seven97,欢迎关注~