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