java中的volatile關鍵字的特性及作用

序言


volatile關鍵字的特性及作用


想要理解volatile關鍵字的作用,需要先對jvm中的內存模型有所了解。
Java內存模型規定,對于多個線程共享的變量,存儲在主內存當中,每個線程都有自己獨立的工作內存(比如CPU的寄存器),線程只能訪問自己的工作內存,不可以訪問其它線程的工作內存。工作內存中保存了主內存共享變量的副本,線程要操作這些共享變量,只能通過操作工作內存中的副本來實現,操作完畢之后再同步回到主內存當中。
對jvm的內存模型有了基本的了解后,再來看volatile關鍵字的幾個重要特性

內存一致性


image.png

假設現在有兩個工作線程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上的機器碼時,它會變成若干條指令.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。