面试准备之volatitle的理解

volatitle这个关键字可以说是面试中必会被问到的问题。
面试官:请说说你对volatitle对是怎么理解的?

我:volatitle可以保证可见性和禁止指令重排序。
可见性:当一个线程对变量作出修改操作后,其他线程对这个修改的结果是立马可以看到的,或者说其他线程再去获取这个变量的时候一定是最新的值。
指令重排序:为了提高执行效率,在不改变单线程执行程序的结果下,java编译器和java处理器会对代码进行重新排序,导致代码书写的顺序和执行顺序不一样。
1、编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
2、处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;

可进行问题引生出来的java内存模型。
如果对没有volatitle修饰的变量做出修改操作后,其他线程获取到的这个值就不是最新的,这是由java内存模型引起的,在每一个线程中还带有一个工作内存,线程对变量进行读写操作的时候,首先会将这个变量从内存中取到工作内存中,再由执行引擎对工作内存中的变量副本操作,操作完后存回到工作内存中,再将工作内存中的值回写到主存中,这里面将变量写回到工作内存,再从工作内存写会到主存中,这两步是可以不连续的,线程可能将变量写到工作内存中就去干其他的事情了,没有将工作线程里面的值马上写入到主存中,别的线程再去内存中取的话就不是最新的值。

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

线程1对i进行重新赋值后是10,这个时候只是将线程1的工作内存中的i改成了10但线程1还没有来得及将工作内存中i=10写入到主存中,这个时候线程2读取主存中的i却还是0,这就是可见性问题。

重排序问题回引发的问题

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

        从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

  也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

volatitle的原理:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

它会强制将对缓存的修改操作立即写入主存;

如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatitle的应用场景
我首先想到的是单例模式的双重检验,这让我想起了11月11日去一个公司面试的时候,面试官让我写一个单例模式,我大手一挥就写出来了一个双重检验的单例模式,但是在申明单例实例对象的时候忘记加关键字volatitle,当时写完我心里还洋洋得意,回去后才发现写错了,面试结果可想而知。

public class Test {

    private static volatile Test test=null;
    private Test(){

    }
    public static Test getSingleTest(){
        if(test==null){
            synchronized (Test.class){
                if(test==null){
                    test = new Test();//这个地方其实是分三步的
                }
            }
        }
        return test;

    }
}

如果不对test加volatitle修饰,这个地方会出现一个什么问题?
test = new Test();是分三步的,或者说new Test();是有两步组成的
1.给新创建的对象分配内存
2.初始花新创建的对象
3.将新创建的对象赋值给test
如果test不带volatitle修饰的话,就有可能出现执行第一步,再执行第三步,最后执行第二步,如果是再单线程的情况下是没有问题的,但是如果第二个线程在第一个线程执行了第一步和第三步,还没有来得及执行第二步的时候去获取Test实例,这个时候是能拿到的,但是拿到的却是半个实例,因为这个实例还没有来得及初始化。

2.状态标记位,当其中第一个线程正在做某个操作的时候,如果这个状态被第二个个线程改变了,第一个线程立马得停止,就可以用到volatitle来修饰状态变量

volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
 
public void setFlag() {
    flag = true;
}

3.开销较低的读-写锁
这个在Java中被广泛用到,例如ActomicInteger,CouncurrentHashMap中,对一个volatitle修饰的变量在写操作的时候加锁,在读的时候不加锁

public class CheesyCounter {  
    // Employs the cheap read-write lock trick  
    // All mutative operations MUST be done with the 'this' lock held  
    @GuardedBy("this") private volatile int value;  
  
    //读操作,没有synchronized,提高性能  
    public int getValue() {   
        return value;   
    }   
  
    //写操作,必须synchronized。因为x++不是原子操作  
    public synchronized int increment() {  
        return value++;  
    }  
}

其中还有很多没有写出来啊,比如原子操作lock,unlock,read,load,use,assign,store,write,为了保证有序性的henpen-before的八大原则,缓存一致性协议mesi,内存屏障这些都没有写到,可能对volatitle的理解还不够深的原因吧。

volatitle和synchronized的不同点:
两者使用的地方不一样:volatitle是用来修饰变量的,synchronized用来修饰方法或者代码块的。
两者所达到的效果不一样:volatitle是保持共享变量的可见性和对修饰变量操作前后代码的有序性,synchronized是保持对共享资源的同步性。
两者产生的后果不一样:volatitle不会产生线程阻塞,synchronized会产生线程阻塞。

参考文:
https://blog.csdn.net/jjavaboy/article/details/77164474
https://www.cnblogs.com/ouyxy/p/7242563.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章