面試準備之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

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