序言
volatile關鍵字的特性及作用
想要理解volatile關鍵字的作用,需要先對jvm中的內存模型有所了解。
Java內存模型規定,對于多個線程共享的變量,存儲在主內存當中,每個線程都有自己獨立的工作內存(比如CPU的寄存器),線程只能訪問自己的工作內存,不可以訪問其它線程的工作內存。工作內存中保存了主內存共享變量的副本,線程要操作這些共享變量,只能通過操作工作內存中的副本來實現,操作完畢之后再同步回到主內存當中。
對jvm的內存模型有了基本的了解后,再來看volatile關鍵字的幾個重要特性
內存一致性
假設現在有兩個工作線程A和B以及一個控制工作流的標志位flag
Thread A;
Thread B;
boolean flag = true;
A線程中對flag做了一些修改(將flag置為false),但此時Thread A只是再修改線程私有的工作內存,ThreadB看不到這個修改,那么此時ThreadB依賴標志位的一些邏輯就將變得不再可靠.
Thread A;
Thread B;
volatile boolean flag = true;
但如果將標志位用volatile關鍵字來修飾,ThreadA再修改標志位flag的時候從主內存中刷新變量的最新值,同時將線程B工作內存中的flag變量置為不可靠狀態(dirty),那么下次ThreadB如果要使用flag標志位的時候就會從主內存中讀取變量的最新值,從而保證了變量再不同線程中的一致性.
防止指令重排
先看下jvm中指令重排的定義:
指令重排序是JVM為了優化指令,提高程序運行效率,在不影響單線程程序執行結果的前提下,盡可能地提高并行度。編譯器、處理器也遵循這樣一個目標。注意是單線程。多線程的情況下指令重排序就會給程序員帶來問題。
關于指令重排,可以通過單例模式的經典模式雙重加鎖來詳細了解:
if(mInstance==null){
synchronized(mObject){
mInstance = new Instance();
}
}
return mInstance;
雙重加鎖單例模式是一種懶漢式的加載模式,為了減少鎖的消耗會再函數入口處提前判空,再在鎖的代碼段落內初始化實例。但實際上初始化的代碼并非原子操作:
mInstance = new Instance()
它是有三條指令組合而成的:
memory =allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance =memory; //3:instance指向剛分配的內存地址,此時對象還未初始化
如果jvm對這三條指令進行指令重排,比如按照 1-3-2 的順序執行,那么在下面這種場景下就會產生問題:
ThreadA調用 getInstance()方法執行完了1和3兩條指令,此時對象未初始化但是mIntance已經指向了一塊內存區域;
ThreadB此時進入getIntance()方法,判定mIntance引用不為空,直接返回。
ThreadB就會使用到未初始化的實例對象,產生不可預期的錯誤.
volatile關鍵字通過禁止指令重排來避免了這種問題的產生.
內存屏障
上面總結到volatile關鍵字可以實現變量在各線程中的一致性,并且具有禁止指令重排的功能。其實這兩個特性是通過內存屏障來實現的.
內存屏障是jvm上的指令,jvm上還有其它指令例如:
(1) lock:將主內存中的變量鎖定,為一個線程所獨占
(2) unclock:將lock加的鎖定解除,此時其它的線程可以有機會訪問此變量
(3) read:將主內存中的變量值讀到工作內存當中
(4) load:將read讀取的值保存到工作內存中的變量副本中。
(5) use:將值傳遞給線程的代碼執行引擎
(6) assign:將執行引擎處理返回的值重新賦值給變量副本
(7) store:將變量副本的值存儲到主內存中。
(8) write:將store存儲的值寫入到主內存的共享變量當中。
從功能上內存屏障可以分為兩種:
- 讀障礙:在某條指令前插入讀障礙指令,保證從主內存中讀取最新值.
- 寫障礙:在某條指令后插入寫障礙指令,保證將緩沖區工作內存中的值寫入到主內存.
而內存屏障指令具體可分為四種:
loadload: 在load1和load2指令之間插入,保障在執行load2之前load1指令的讀取操作完成.
storestore: 在store1和store2指令之間插入,保障在執行store2之前store1指令的寫入操作對其它線程(處理器)可見.
loadstore: 在load1和store2指令之間插入,保障在執行store2之前load1指令的讀取操作完成.
storeload 在store1和load2指令之間插入,保障在執行load2之前store1指令的寫入操作完成并對其它線程(處理器)可見.
volatile關鍵字的注意事項
volatile關鍵字使得對于單個變量的讀寫操作具有了原子性,但不包括自增自減這種操作。也就是說
volatile a;
a = 1;
b = a;
在語義上等同于
sychronized(){
a = 1;
}
synchronized(){
b =a;
}
但
volatile a;
a++;
這種操作并不具有原子性,因為a++并非一條指令!,當翻譯成jvm上的機器碼時,它會變成若干條指令.