深入理解volatile关键字

一、简介

volatile是 Java提供的一种轻量级的同步机制。Java包含两种内在的同步机制:同步块(或方法)和 volatile 变量相比于synchronized (synchronized常称为重量级)volatile是更轻量级的,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差,而且其使用也更容易出错。

二、可见性问题

1. 可见性案列

public class TestVolatile {
    private  static boolean flag = false;
    //private volatile static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            flag = true;
            System.out.println("=======循环之前=======");
            while (flag) {

            }
            System.out.println("=======循环之后=======");
        });
        thread1.start();

        Thread.sleep(200);

        Thread thread2 = new Thread(() -> {
            System.out.println("修改flag之前...");
            System.out.println(flag); // true

            flag = false;
            System.out.println("修改flag之后...");
            System.out.println(flag);  // false 上面的线程没有跳出循环
        });
        thread2.start();
    }
}

这段代码的作用就是当启用thread1时进入一个线程,然后休眠一段时间确保thread2 的运行在thread1之后。然后启用thread2让thread2修改flag值让程序退出循环。但这个程序会按照我们想要的方式运行吗?
在这里插入图片描述

看程序运行的结果可以发现程序并没有退出循环,也就是说thread2修改了值,thread1并不知道,所以在thread1中flag的值还是为true。为什么会出现这样的情况呢?这就要谈到我们的JMM内存模型了。

2. JMM内存模型

JMM 决定一个线程对共享变量的写入何时对另一个线程可见, J M M 定义了线程和主内存之间的抽象关系:共享变量存储在主内存( M ain Memory) 中,每个线程都有一个私有的本地内存 (Local Memory) ,本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都须在工作内存中进行,而不能直接读写主内存中的变量。

在这里插入图片描述

从图中可以知道我们每个线程在工作时用到的都是工作内存,当thread2修改flag值时,并没有把修改的值同步到主内存中,而thread也无法从主内存中读取到thread2的修改值,所以thread1他并不知道flag的值被修改成了false。那要怎么解决这个问题呢?解决这种共享变量在多线程椟型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用 synchronized 或者 Lock 这些方式太重量级了,比较合理的方式其实就是vo|atile。那vo|atile是怎么解决可见性问题呢?

3. 解决办法

public class TestVolatile {
//    private  static boolean flag = false;
    private volatile static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            flag = true;
            System.out.println("=======循环之前=======");
            while (flag) {

            }
            System.out.println("=======循环之后=======");
        });
        thread1.start();

        Thread.sleep(200);

        Thread thread2 = new Thread(() -> {
            System.out.println("修改flag之前...");
            System.out.println(flag); // true

            flag = false;
            System.out.println("修改flag之后...");
            System.out.println(flag);  // false 上面的线程没有跳出循环
        });
        thread2.start();
    }
}

在这里插入图片描述

当我们把变量使用 volatile 修饰时 private volatile static boolean flag = false;,thread2对变量进行操作时,会把变量变化的值强制刷新的到主内存。当thread1获取值时,会把自己的内存里的 flag值过期掉,之后从主内存中读取。所以添加关键字后程序如预期输出结果。

在这里插入图片描述

从运行的结果上来看我们的程序已经退出了循环volatile的确是解决了可见性问题。那除了可见性问题,voliate还可以解决什么样的问题呢?

三、指令重排问题

Java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。从 Java 源代码到最终执行的指令序列,会分别经历下面3种重排序:

在这里插入图片描述

1. 指令重排案列

public class Singleton {
    private static Singleton singleton;
    public static Singleton getSingleton(){
        if (Objects.isNull(singleton)){
            //有可能很多线程阻塞到拿锁,拿完锁再判断一次
            synchronized (Singleton.class){
                if (Objects.isNull(singleton)){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

以上代码是一个单列模式,但是这个单列模式有一个大问题,那就是 singleton = new Singleton()这行代码会出现指令重排。我么可以理解为了以下3行代码。

a. memory = allocate() //分配内存
b. ctorInstanc(memory) //初始化对象
c. singleton = memory   //设置instance指向刚分配的地址

假设A、B两个线程同时执行代码。当A线程执行到singleton = new Singleton()这行代码时,发生了指令排序A线程执行了a、c代码没有执行b代码,此时B线程正好执行到 if (Objects.isNull(singleton))这个判断语句。因为A线程执行了c代码所以singleton不等于空,因此B线程就会返回一个没有初始化的singleton对象。

2. 解决问题

解决指令重排我们也可以在if (Objects.isNull(singleton))前面再加一个锁,但是这样的解决办法也太重量级了。因此我们也可以使用volatile来解决指令重排问题。那volatile是如何解决指令重排问题?

public class Singleton {
    private volatile static Singleton singleton;
    public static Singleton getSingleton(){
        if (Objects.isNull(singleton)){
            //有可能很多线程阻塞到拿锁,拿完锁再判断一次
            synchronized (Singleton.class){
                if (Objects.isNull(singleton)){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

3.内存屏障

内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。JMM提供了4种内存屏障。

屏障类型指令示例说明
LoadLoadLoad1; LoadLoad; Load2保证load1的读取操作在load2及后续读取操作之前执行
StoreStoreStore1; StoreStore; Store2在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStoreLoad1; LoadStore; Store2在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoadStore1; StoreLoad; Load2保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略

  • 在每个volatile写操作的前面插入一个StoreStore屏障。禁止上面的普通写和下面的volatile写重排序。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。防止上面的volatile写与下面可能有的volatile读/写重排序。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。禁止上面的volatile读和下面所有的普通读操作重排序。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。禁止上面的volatile读和下面所有的普通写操作重排序。

正是有了内存屏障的存在,才能让volatile能够禁止指令重排的问题。

四、总结

  • volatile会控制被修饰的变量在内存操作上主动把值刷新到主内存,JMM 会把该线程对应的CPU内存设置过期,从主内存中读取最新值。
  • volatile 的内存屏故障是在读写操作的前后各添加一个 StoreStore屏障,也就是四个位置,来保证重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置。
  • volatile 并不能解决原子性,如果需要解决原子性问题,需要使用 synchronzied 或者 lock。
  • 13
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

努力发光的程序员

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

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

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

打赏作者

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

抵扣说明:

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

余额充值