volatile关键字原理

volatile

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

  1. 保证内存可见性
  2. 禁止指令重排序,保证有序性

1.内存可见性

Java 内存模型

首先我们需要理解Java 内存模型(Java Memory Model)简称为 JMM。

JMM主要是用来屏蔽不同硬件和操作系统的内存访问差异的,因为在不同的硬件和不同的操作系统下,内存的访问是有一定的差异得,这种差异会导致相同的代码在不同的硬件和不同的操作系统下有着不一样的行为,而 Java 内存模型就是解决这个差异,统一相同代码在不同硬件和不同操作系统下的差异的。

Java 内存模型规定:所有的变量(实例变量和静态变量)都必须存储在主内存中,每个线程也会有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量

Java 内存模型会带来一个新的问题,那就是内存可见性问题,也就是当某个线程修改了主内存中共享变量的值之后,其他线程不能感知到此值被修改了,它会一直使用自己工作内存中的“旧值”。

private static boolean flag = false;
public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (!flag) {

            }
            System.out.println("终止执行");
        }
    });
    t1.start();
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("设置 flag=true");
            flag = true;
        }
    });
    t2.start();
}

以上代码我们预期的结果是,在线程 1 执行了 1s 之后,线程 2 将 flag 变量修改为 true,之后线程 1 终止执行,然而,因为线程 1 感知不到 flag 变量发生了修改,也就是内存可见性问题,所以会导致线程 1 会永远的执行下去

如何解决以上问题呢?只需要给变量 flag 加上 volatile 修饰即可,这就是 volatile 的内存可见性。

可见性的实现原理

volatile 内存可见性主要通过 lock 前缀指令实现的,它会锁定当前内存区域的缓存(缓存行),并且立即将当前缓存行数据写入主内存(耗时非常短),回写主内存的时候会通过 MESI 协议使其他线程缓存了该变量的地址失效,从而导致其他线程需要重新去主内存中重新读取数据到其工作线程中。

MESI 协议,全称为 Modified, Exclusive, Shared, Invalid,是一种高速缓存一致性协议。它是为了解决多处理器(CPU)在并发环境下,多个 CPU 缓存不一致问题而提出的。

2.有序性

有序性也叫做禁止指令重排序。指令重排序是指编译器或 CPU 为了优化程序的执行性能,而对指令进行重新排序的一种手段。指令重排序的实现初衷是好的,但是在多线程执行中,如果执行了指令重排序可能会导致程序执行出错。

指令重排序最典型的一个问题就发生在单例模式中,比如以下问题代码:

public class Singleton {
    private Singleton() {}
    private static Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) { // ①
            synchronized (Singleton.class) {
            	if (instance == null) {
                	instance = new Singleton(); // ②
                }
            }
        }
        return instance;
    }
}

以上问题发生在代码 ② 这一行“instance = new Singleton();”,这行代码看似只是一个创建对象的过程,然而它的实际执行却分为以下 3 步:

  1. 创建内存空间。
  2. 在内存空间中初始化对象 Singleton。
  3. 将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。

如果此变量不加 volatile,那么线程 1 在执行到上述代码的第 ② 处时就可能会执行指令重排序,将原本是 1、2、3 的执行顺序,重排为 1、3、2。但是特殊情况下,线程 1 在执行完第 3 步之后,如果来了线程 2 执行到上述代码的第 ① 处,判断 instance 对象已经不为 null,但此时线程 1 还未将对象实例化完,那么线程 2 将会得到一个被实例化“一半”的对象,从而导致程序执行出错,这就是为什么要给私有变量添加 volatile 的原因了。

要使以上单例模式变为线程安全的程序,需要给 instance 变量添加 volatile 修饰。

有序性的实现原理

volatile 的有序性是通过插入内存屏障(Memory Barrier),在内存屏障前后禁止重排序优化,以此实现有序性的。

什么是内存屏障?

内存屏障(Memory Barrier 或 Memory Fence)是一种硬件级别的同步操作,它强制处理器按照特定顺序执行内存访问操作,确保内存操作的顺序性,阻止编译器和 CPU 对内存操作进行不必要的重排序。

内存屏障可以确保跨越屏障的读写操作不会交叉进行,以此维持程序的内存一致性模型。

volatile 通过内存屏障的插入来实现:

  • 写内存屏障(Store Barrier / Write Barrier): 当线程写入 volatile 变量时,JMM 会在写操作前插入 StoreStore 屏障,确保在这次写操作之前的所有普通写操作都已完成。接着在写操作后插入 StoreLoad 屏障,强制所有后来的读写操作都在此次写操作完成之后执行,这就确保了其他线程能立即看到 volatile 变量的最新值。
  • 读内存屏障(Load Barrier / Read Barrier): 当线程读取 volatile 变量时,JMM 会在读操作前插入 LoadLoad 屏障,确保在此次读操作之前的所有读操作都已完成。而在读操作后插入 LoadStore 屏障,防止在此次读操作之后的写操作被重排序到读操作之前,这样就确保了对 volatile 变量的读取总是能看到之前对同一变量或其他相关变量的写入结果。

通过这种方式,volatile 关键字有效地实现了内存操作的顺序性,从而保证了多线程环境下对 volatile 变量的操作遵循 happens-before 原则,确保了并发编程的正确性。

原文链接:https://www.javacn.site/interview/thread/volatile-principle.html

使用volatile的场景

修饰的对象

  1. 修饰基础类型,例如int、boolean、long等
  2. 修饰对象引用,这里是防止对象引用做了变更,其他线程不可见,例如上述的单例模式。注意volatile不能保证对象内部状态的可见性,对于对象内部的属性,可见性是不生效的,因此如果是对象本身的线程安全,需要用synchronized。

使用场景:

  1. 状态标志(boolean):多个线程需要共享一个简单的状态标志,使用volatile确保一个线程的修改对其他线程立即可见。
  2. 单例模式的双重检查锁定
  3. "读多写少"的并发场景
  4. 独立操作:当变量的写操作不依赖于当前值时

不适用的场景:

  1. 复合操作:例如i++
  2. 依赖于当前值的写操作:value = value + 5;
  3. 需要保证多个变量同时更新的原子性:volatile只能保证单个变量的原子性

使用volatile的注意事项

要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
1,对变量的写操作不依赖于当前值。比如X++就不行
2,该变量没有包含在具有其他变量的不变式中(限制范围的条件也不行,如X>Y)。

为什么呢?

因为volatile并不能保证其原子性,他只保证了某一个线程对他修改以后其他线程可见。

对于 x=1,这个操作来说,是原子操作,但是对于 x++ 来说,它不是原子操作。

这样的话,对于这种情况,线程1读取到x的最新值为100,需要进行++操作,但是读完之后它就阻塞住了。线程2这时进行了++,将x改为101,但是线程1已经读取了值为100,它会进行x=101的赋值,因为它已经读了最新值,不会再读一遍。

volatile与synchronized的区别

1,volatile不会造成线程的阻塞,synchronized会。

2,synchronized会 造成线程状态的改变,而线程状态的改变又依赖于操作系统,所以效率会比较低。

3,synchronized可以修饰代码块、方法。volatile只能修饰变量。

4,synchronized能保证原子性、volatile不能。

5,volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

6,使用volatile而不是synchronized的唯一安全情况是类中只有一个可变的域。

原文:https://blog.youkuaiyun.com/qq_37937537/article/details/82764667

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值