Java並發-volatile的原理及用法
volatile屬性:可見性、保證有序性、不保證原子性。
一、volatile可見性
在Java的內存中所有的變量都存在主內存中,每個線程有單獨CPU緩存內存,多個線程對同一個變量讀取時,會從主內存中把變量拷貝到自己的CPU緩存中,線程之間也無法直接訪問對方CPU緩存內存中的變量,只能通過主內存傳遞變量的值;
舉個例子、例一;
1 int i=0; 2 //線程一中執行 3 i=1; 4 //線程二中執行 5 int j=0; 6 j=i;
上面這個程序在線程一中,讀取內存變量i的值到線程一的CPU緩存中,線程一的CPU緩存中並將i的值設為1,這時還沒來的及將i的值從線程一的CPU緩存中寫入到主內存中時,此時線程二從主內存中讀取到i的值到線程二的CPU緩存中,此時i還是0,線程二並沒有及時得到線程一修改的值,這就是不可見性;
如果把上面程序的int i=0換成volatile int i=0,就可以保證線程二可以立刻可見線程一修改i后的值,因為JVM 保證了每次讀變量都從內存中讀,相當於跳過 CPU cache 這一步,讓線程一直接修改主內存中i的值;
但是如果將i換成volatile int i,將i=1換成i++,線程二對線程一的改變i后的值仍是不可見的,原因就是下面要說的原子性;
二、volatile不可保證原子性
原子性就是指操作不可分割,比如i=1這個就不可分割,i++就包含三個操作:讀取i的值,進行加1操作,寫入新的值,這就可分割不是原子操作。如果將i換成volatile int i,將i=1換成i++,當線程一執行到對i加1操作時,還沒有來得及寫入新值時,線程二執行j=i,線程獲取的i值任然為0;可以通過synchronized和Lock來實現更大范圍操作的原子性。
三、volatile保證有序性
有序性,及程序按照編寫的順序先后執行。
舉個例子、例二:
1 int i=0;//1 2 int j=1;//2 3 i++;//3 4 j++;//4
上面這個程序按照正常邏輯執行順序應該是1>2>3>4;但是在JVM中並一定不會按照這個順序執行,因為處理器為了提高運行效率,在JVM中的及時編譯存在指令重排序的優化,它會改變各個語句的執行順序,但是不改變運行結果,就是改變后的順序運行結果和改變前的結果一樣,就行如果按照1>3>2>4,雖然改變順序,但並不影響結果;
如果把程序改變成這個樣子、例三:
1 int i=0;//1 2 int j=1;//2 3 i++;//3 4 j=i+1;//4
上面這個程序執行順序一定是1、2在3前面,3一定會在4前面,1和2順序可變,因為處理器在進行重排序時是會考慮指令之間的數據依賴性,如果一個指令指令2必須用到指令1的結果,那么處理器會保證指令1會在指令2之前執行。
重排序雖然對單線程沒影響但是對多線程就會產生影響,使結果不一樣;
舉個例子、例四:
1 User user;//1 2 boolean flag=flase;//2 3 //線程一 4 user=new User();//3 5 flag=true;//4 6 //線程二 7 if(flag){ 8 user.name="張振力";//5 9 }
上面這個程序的意思就是,如果在線程一user完成初始化,就把flag設置為true,在線程二中如果flag為true,就初始化user.name的值;但是在實際中因為3和4並沒有依賴,會被重排序,語句4先執行,語句3還沒執行,在線程二中得到user值就會為空,這時候給name賦值就會報錯。
通過定義volatile就可以保證語句3一定在語句4前執行,就是因為volatile定義的變量在賦值時,在其上面的程序是一定完成過的,在其后的程序一定是沒完成的,而且其前完成的操作對其后的是可見的;舉個例子、例五:
1 volatile boolean flag =false; 2 int i,j,x,y; 3 i=10;//1 4 j=10;//2 5 flag=true;//3 6 x=10;//4 7 y=10;//5
上面的例子執行的順序就是在3之前1和2語句已經執行完(不保證1、2的順序),但4和5語句(不保證4、5的順序)肯定還沒執行;
再返回例四里面看,如果flag定義為volatile,那肯定能保證語句3一定在語句4前執行,如果flag為true,那user肯定完成了初始化,就不會報錯。
1 User user;//1 2 volatile boolean flag=flase;//2 3 //線程一 4 user=new User();//3 5 flag=true;//4 6 //線程二 7 if(flag){ 8 user.name="張振力";//5 9 }
四、volatile實現可見性和有序性的原理:
1.可見性
如果對聲明了volatile變量進行寫操作時,JVM會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫會到系統內存。 這一步確保了如果有其他線程對聲明了volatile變量進行修改,則立即更新主內存中數據。
2.有序性
但這時候其他處理器的緩存還是舊的,所以在多處理器環境下,為了保證各個處理器緩存一致,每個處理會通過嗅探在總線上傳播的數據來檢查 自己的緩存是否過期,當處理器發現自己緩存行對應的內存地址被修改了,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操作時,會強制重新從系統內存把數據讀到處理器緩存里。 這一步確保了其他線程獲得的聲明了volatile變量都是從主內存中獲取最新的。
在這里就講這一個使用場景:狀態標記,后續會再寫另一個使用場景,單例模式下的volatile確保單例對象的返回的正確性。