前言
要學習好Java的多執行緒,就一定得對volatile關鍵字的作用機制了熟於胸。最近博主看了大量關於volatile的相關部落格,對其有了一點初步的理解和認識,下面通過自己的話敘述整理一遍。
有什麼用?
volatile主要對所修飾的變數提供兩個功能
- 可見性
- 防止指令重排序
本篇部落格主要對volatile可見性進行探討,以後發表關於指令重排序的博文。
什麼是可見性?
一圖勝千言
上圖已經把JAVA記憶體模型(JMM)展示得很詳細了,簡單概括一下- 每個Thread有一個屬於自己的工作記憶體(可以理解為每個廚師有一個屬於自己的鐵鍋)
- 所有Thread共用一個主記憶體(餐廳所有的廚師共用同一個冰箱)
- 每個Thread運算元據之前都會去主記憶體中獲取資料(廚師炒菜之前都要去冰箱裡拿食材)
- Thread:廚師
- 工作記憶體:鐵鍋
- store&load:放熟食,取食材
- 主記憶體:冰箱
讀者可思考以下情景:
餐廳來了一位顧客點了一份紅燒肉,此時有兩位大廚(假設大廚之間互不通訊),由於互不通訊,所以兩位大廚都開啟冰箱取出食材開始炒菜。
最後炒出了兩份紅燒肉,顧客只要一份。為什麼會造成這種結果?
由於大廚之間沒有可見性。
將此情景放在JAVA中即是:
執行緒A從主記憶體中取了一個變數到工作記憶體中,操作完畢後沒有及時放回主記憶體中,於是執行緒B去取這個變數已經過期了,取的是執行緒A操作之前的變數。
如何擁有可見性?
先介紹一下Java記憶體模型中定義的8種工作記憶體與主記憶體之間的原子操作
- lock( 鎖定 ):作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔的狀態。
- unlock(解鎖):作用於主記憶體的變數,把一個處於鎖定的變數釋放出來,釋放變數才可以被其他執行緒鎖定。
- read(讀取):作用於主記憶體的變數,把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。
- load(載入):作用於***工作記憶體***的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
- use(使用):作用於***工作內***存種的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。
- assign(賦值):作用於***工作記憶體***中的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
- store(儲存):作用於***工作記憶體***的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用
- write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中得到的值放入主記憶體的變數中。
讀取賦值一個普通變數的情況
當執行緒1對主記憶體物件發起read操作到write操作套流程的時間裡,執行緒2隨時都有可能對這個主記憶體物件發起第二套操作- 有什麼危害呢?
假設主記憶體中有一個
int a=0;
複製程式碼
執行緒1和執行緒2分別執行一次,理想狀態下最終a的值為2.
a++;
複製程式碼
執行緒1在執行了assign操作之後變數a的真實值已經從0變成了1,但是這個過程發生在工作記憶體中對其他執行緒不可見,若執行緒2此時對變數a的操作,讀取到的值仍然為0,因為沒有可見性,執行緒2的操作也僅僅是重複了執行緒1的操作,再次讓a從0變成了1。並沒有達到期望的a=2。
讀取賦值一個volatile變數的情況
volatile變數對物件的操作更嚴格:- use之前不能被read&load
- assign之後必須緊跟store&write
也就是說 read-load-use 和 assign-store-write成為了兩個不可分割的原子操作
儘管這時候在use和assign之間依然有一段真空期,有可能變數會被其他執行緒讀取,但是無論在哪一個時間點主記憶體的變數和任一工作記憶體的變數的值都是相等的。這個特性就導致了volatile變數不適合參與到依賴當前值的運算,如自增。 那麼依靠可見性的特點volatile可以用在哪些地方呢? 《Java虛擬機器》提到:
運算結果並不依賴變數的當前值(即結果對產生中間結果不依賴),或者能夠確保只有單一的執行緒修改變數的值
通常volatile用做儲存某個狀態的boolean值。
部分參考自
- volatile變數與普通變數的區別
- <<深入理解Java虛擬機器 高階特性與最佳實踐>>