天天看点

volatile关键字的作用volatile怎么用,有什么用保持内存可见性防止指令重排

volatile关键字的作用

  • volatile怎么用,有什么用
  • 保持内存可见性
  • 防止指令重排

volatile怎么用,有什么用

volatile关键字用与修饰全局变量(类中方法外的变量),放在数据类型前面
   例: 
           
volatile boolean  flag = false;
   volatile private int  a = 1; 
           
volatile关键字可以保持内存的可见性与指令重排
           

保持内存可见性

什么是保持内存可见性

内存可见性:是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

这样说看起来可能一脸懵逼,下面看一段代码吧

public class VolatileTest extends Thread {
    boolean  flag = false;
    int i = 0;
   // 调用start开启一个新的线程运行该方法里面的代码
    public void run() {
        //判断flag的值如果问true就停止循环
        while (!flag) {
            i++;
        }
    }

    public static void main(String[] args) throws Exception {
        //创建vaoltileTest类对象
        VolatileTest vt = new VolatileTest();
        //开启一个新的线程
        vt.start();
        //主线程调用sleep方法 停止运行 2000毫秒
        Thread.sleep(2000);
        //设置全局变量 flage的值问true
        vt.flag = true;
        //输出 全局变量i的值
        System.out.println("stope" + vt.i);
    }
}
           

大家先想一下该方法运行的结果会是怎么样的会有什么结果?

你们的答案可能是控制台输出 “stope(i的值)”方法停止运行执行完毕,i的值等于或大于0

可惜答案并非你们想的那样,运行这个代码你会发现控制台输出“stope(i的值)”,但是方法并没有停止运行,而是一直在运行没有结束。

你debug会发现,另一个线程一直没有结束,一直在循环,这个时候你就会觉得很疑惑了,我明明在main方法中把flag的设为了true但是run方法里面获取的flag值为什么一直是false呢?

那么现在就来分析一下run方法中获取的flag值为什么是false

看看下面的线程内存模型图

volatile关键字的作用volatile怎么用,有什么用保持内存可见性防止指令重排

每一个线程都有一个线程工作内存,但是他们共用一个主内存,线程中的共享变量都是放在主内存中的,线程使用共享变量的时候会去主内存中去加载需要使用到的变量值,再次读取变量是从线程工作内存中读取而不是去主内存中读取(是不是似乎明白点什么了)

VolatileTest vt = new VolatileTest();

这一行大家都知道,创建VolatileTest类对象

vt.start();

开启一个新的线程,执行该类中的run方法,也就是执行run方法里面那个循环

Thread.sleep(2000);

主线程休眠2000毫秒,在主线程休眠这段时间,虚拟机有足够的时间执行run方法里面的代码,所以在这段时间内就没办法吧flag的值设置为true

while (!flag)

执行到这一段代码虚拟机会把主内存中的flag值加载到线程工作内存中,因为此时主线程在休眠中,所以flag的值还是false,循环的条件成立,执行循环中的代码( i++;)

vt.flag = true;

线程休眠结束,执行这一行代码,把flag的值从主内存中加载到线程工作内存中,修改为true,把flag=true重新写回主内存中

System.out.println(“stope” + vt.i);

输出 “stope (i的值)”,这一句运行完主线程就结束了

导致上面程序没有停止运行的原因就是因为,在flag为false的时候已经把flag的值读取到线程工作内存中,再次使用到该值没有去主内存中读取,而是一直读取线程工作内存中的值,所以flag的就一直为false,循环条件一直成立,一直循环导致程序一直没有结束

怎么样才能让这个程序成功结束呢?

只要我们在flag变量前加上volatile关键字就可以了

volatile boolean flag = false;

volatile关键字可以保存内存可见性,意思就是每个线程每次获取volatile关键字修饰的变量都会是他的最新值

用volatile修饰的变量,线程每次获取都会从主内存中去加载,而不会去线程工作内存中去加载

防止指令重排

什么是指令重排

是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段

列:

int k = 0; 
int j = 1  
k = 5; //代码1
j = 6; //代码2
           

按照有序性(一个线程中的所有操作必须按照程序的顺序来执行)的规定,代码1应该执行在代码2之前,但实际情况是这样的吗?

不一定,因为代码1并不依赖代码2,先执行哪一行代码结果都是一样的,所以jvm就有可能进行指令重排

指令重排会重出现什么问题?

在单线程下不影响,但是在多线程的情况下可能影响执行结果,volatile关键字可以防止指令重排

列:

//单例模式
public class SingleTest {
    private static SingleTest instance ;
    private SingleTest(){
    }
    public static SingleTest getInstance(){
        if(instance == null){
             synchronized (SingleTest.class){
                 if(instance == null){
                     instance = new SingleTest();
                 }
             }
        }
            return instance ;
    }
}
           

上面使用到的是DCL(Double Check Lock 双重检查锁),看上去这个单列模式是没有问题,可是远远没这么简单,在多线程的情况下你可能只获取到“半个”单列(没有初始化的对象)

问题出在这一句代码上

singleTest = new SingleTest();

这一句不是原子操作,可以抽象为几条jvm命令

memory = allocate(); //1:分配对象的内存空间

initInstance(memory); //2:初始化对象

instance = memory; //3:设置instance指向刚分配的内存地址

因为2和3没有相互依赖,所以可能会进行指令重排

memory = allocate(); //1:分配对象的内存空间

instance = memory; //3:设置instance指向刚分配的内存地址

initInstance(memory); //2:初始化对象

如果指令重排为上面这种情况,当执行到3的时候instance已经不为空了,但是还没有初始化,如果这个时候另一个线程调用了这个方法就会获取到一个没有初始化的对象,从而影响执行结果,如果想防止指令重排这种情况出现,在全局变量上加上volatile关键字就行了

参考:

https://zhuanlan.zhihu.com/p/34362413

https://blog.csdn.net/zezezuiaiya/article/details/81456060

https://www.cnblogs.com/shan1393/p/8999683.html

https://www.cnblogs.com/xd502djj/p/9873067.html

继续阅读