volatile 关键字

如何保证变量的可见性

  • 在 Java 中,volatile 关键字可以保证变量的可见性,如果变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取

如何禁止指令重排

  • 在 Java 中,volatile 还有一个重要的作用就是防止 JVM 的指令重排序,如果将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的内存屏障的方式来禁止指令重排序

  • 在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:
    public native void loadFence();
    public native void storeFence();
    public native void fullFence();

    理论上来说,通过这个三个方法也可以实现和 volatile 禁止重排序一样的效果,只是会麻烦一些

  • 禁止指令重排效果演示

// 双重检查实现单例模式
public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
  • 执行步骤 (uniqueInstance 采用 volatile 修饰)
uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
为 uniqueInstance 分配内存空间
初始化 uniqueInstance
将 uniqueInstance 指向分配的内存地址
  • 由于 JVM 具有指令重排的特性,执行顺序有可能变成 1 ⇒ \Rightarrow 3 ⇒ \Rightarrow 2

  • 指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例

  • 例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化


volatile 是否可以保证原子性

  • volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的

  • 假设对变量 inc 进行 ++ 操作,inc++ 其实是一个复合操作,包括三步:
    读取 inc 的值
    对 inc 加 1
    将 inc 的值写回内存

  • volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:
    线程 1 对 inc 进行读取操作之后,还未对其进行修改
    线程 2 又读取 inc 的值并对其进行修改(+1),再将 inc 的值写回内存
    线程 2 操作完毕后,线程 1 对 inc 的值进行修改(+1),再将 inc 的值写回内存

  • 这就导致两个线程分别对 inc 进行一次自增操作后,inc 实际上只增加 1

  • 如果想要保证上面的代码运行正确也非常简单,利用 synchronized 、Lock 或者 AtomicInteger 都可以


内存屏障

  • 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种:源代码 ⇒ \Rightarrow 编译器优化的重排 ⇒ \Rightarrow 指令并行的重排 ⇒ \Rightarrow 内存系统的重排 ⇒ \Rightarrow 最终执行指令

  • 单线程环境最终执行结果和代码顺序的结果一致

  • 处理器在进行重排序时,必须要考虑指令之间的数据依赖性

  • 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

  • volatile 实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象

  • 内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:保证特定操作的顺序 和 保证某些变量的内存可见性
    1、由于编译器和处理器都能执行指令重排的优化,如果在程序中插入一条Memery Barrier(内存屏障),那么就会告诉编译器和CPU不能对这条指令进行重排
    2、也就是说通过插入内存屏障,使屏障前后的指令不会进行重排优化
    3、内存屏障还可以强制刷出CPU的缓存,因此CPU上的线程都能读到这些数据的最新版本

  • 内存屏障分为四种
    StoreStore 屏障、StoreLoad 屏障、LoadLoad 屏障、LoadStore 屏障

  • Load 相当于读屏障,Store 相当于写屏障

  • 工作流程


参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tytler

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值