注:本文內容會有部分涉及上節的硬件知識:【Java并發學習二】多線程編程的硬件基礎知識總結
1. 內存屏障
上一節講過,為了解決寫緩沖器和無效化隊列帶來的有序性和可見性問題,我們引入了內存屏障。
內存屏障是被插入兩個CPU指令之間的一種指令,用來禁止處理器指令發生重排序(像屏障一樣),從而保障有序性的。另外,為了達到屏障的效果,它也會使處理器寫入、讀取值之前,將寫緩沖器的值寫入高速緩存,清空無效隊列,從而“附帶”的保障了可見性。
舉個例子說明:
?????????Store1?? Store2?? Load1?? StoreLoad屏障?? Store3?? Load2?? Load3
對于上面的一組CPU指令(Store表示寫入指令,Load表示讀取指令),StoreLoad屏障
之前的Store
指令無法與StoreLoad屏障
之后的Load
指令進行交換位置,即重排序。但是StoreLoad屏障
之前和之后的指令是可以互換位置的,即Store1可以和Store2互換,Load2可以和Load3互換。
StoreLoad屏障
的目的在于使屏障前的寫操作的結果,對于屏障后的讀操作是可見的。為了保障這一點,除了指令不能重排序外,StoreLoad屏障
還會在寫操作完之后,將寫緩沖器中的條目沖刷入高速緩存或主內存;在讀操作之前,清空無效化隊列,從主內存或其他處理器的高速緩存中讀取最新值到自己的內存。從而保障了數據在不同處理器之間是一致的,即可見性。
1.1 基本內存屏障
基本內存屏障可以分為:LoadLoad屏障、LoadStore屏障、StoreStore屏障和StoreLoad屏障。這些屏障可統一用XY來表示,XY屏障的作用是禁止屏障左側的任何X操作與屏障右側的任何Y操作之間進行重排序。
LoadLoad屏障:
對于這樣的語句 Load1; LoadLoad; Load2,在Load2及后續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
StoreStore屏障:
對于這樣的語句 Store1; StoreStore; Store2,在Store2及后續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
LoadStore屏障:
對于這樣的語句Load1; LoadStore; Store2,在Store2及后續寫入操作被執行前,保證Load1要讀取的數據被讀取完畢。
StoreLoad屏障:
對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的(沖刷寫緩沖器,清空無效化隊列)。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。
1.2 內存屏障的分類
按照可見性保障來劃分
內存屏障可分為:加載屏障(Load Barrier)和存儲屏障(Store Barrier)。
加載屏障:StoreLoad
屏障可充當加載屏障,作用是刷新處理器緩存,即清空無效化隊列,使處理器在讀取共享變量時,先從主內存或其他處理器的高速緩存中讀取相應變量,更新到自己的緩存中
存儲屏障:StoreLoad
屏障可充當存儲屏障,作用是沖刷處理器緩存,即將寫緩沖器內容寫入高速緩存中,使處理器對共享變量的更新寫入高速緩存或者主內存中
這兩個屏障一起保證了數據在多處理器之間是可見的。按照有序性保障來劃分
內存屏障分為:獲取屏障(Acquire Barrier)和釋放屏障(Release Barrier)。
獲取屏障:相當于LoadLoad屏障
與LoadStore屏障
的組合。在讀操作后插入,禁止該讀操作與其后的任何讀寫操作發生重排序;
釋放屏障:相當于LoadStore屏障
與StoreStore屏障
的組合。在一個寫操作之前插入,禁止該寫操作與其前面的任何讀寫操作發生重排序。
這兩個屏障一起保證了臨界區中的任何讀寫操作不可能被重排序到臨界區之外。
2. volatile原理
Java中,volatile
保障了所修飾變量的可見性和有序性,下面用內存屏障來解釋下它的具體實現原理。
2.1 volatile變量的讀寫過程
-
寫操作:
如上圖,釋放屏障(LoadStore屏障
與StoreStore屏障
)保證了volatile寫操作與該操作之前的任何讀、寫操作都不會進行重排序。從而保證了volatile寫操作之前,任何的讀寫操作都會先于volatile被提交。
而存儲屏障(StoreLoad屏障
)除了使volatile寫操作不會與之后的讀操作重排序外,還會沖刷處理器緩存,使volatile變量的寫更新對其他線程可見,該內存屏障與讀操作的加載屏障一起保障了可見性。
-
讀操作:
如上圖,加載屏障(StoreLoad屏障
)除了使volatile讀操作不會與之前的寫操作發生重排序外,還會刷新處理器緩存,使volatile變量讀取的為最新值。
獲取屏障(LoadLoad屏障
與LoadStore屏障
)禁止了volatile讀操作與其之后的任何讀寫操作進行重排序。保障了volatile變量讀操作之后的任何讀寫操作,volatile的寫線程的更新已經對其可見。
2.2 舉例說明
volatile對有序性的保障,看了上面的圖后,可能還是有點亂,下面我們舉個例子來說明:
寫線程 | 讀線程 |
---|---|
A = 1; B = 2; [LoadStore + StoreStore] //釋放屏障 V = true; |
|
if(V){ ???[LoadLoad + LoadStore] //獲取屏障 ???sum = A + B; } |
如上表,寫線程、讀線程交替執行。其中A、B為普通變量,V為volatile修飾的變量。
寫線程的釋放屏障(LoadStore
與StoreStore
)確保了A、B變量的更新先于V的更新被提交,這樣便確保了讀線程在讀到V的更新值時,也能讀到A、B的更新值。
讀線程的獲取屏障(LoadLoad
與LoadStore
)確保了讀線程一定是先讀取V,再讀取A、B變量。這樣做的原因在于,為了保障讀線程能夠感知到寫線程的正確寫入順序,讀線程讀取變量的順序要與寫線程寫入變量的順序相反(即讀V——>讀B——>讀A)
寫讀線程通過釋放屏障和獲取屏障的配對使用,保障了volatile變量的有序性。
2.3 volatile的應用場景
最后補充下volatile關鍵字的應用場景:
- 使用volatile變量作為狀態標志
- 使用volatile保障變量的可見性
- 使用volatile變量替代鎖。將一組可變狀態變量封裝成volatile修飾的實體對象,每次更改時,重新給這個實體對象賦值。
- 使用volatile實現簡易版讀寫鎖
3. synchronized原理
synchronized編譯成字節碼后,是通過monitorenter
(入鎖)和monitorexit
(出鎖)兩個指令實現的,具體過程如下:
可以發現,與volatile類似,synchronized底層也是通過釋放屏障和獲取屏障的配對使用保障有序性,加載屏障和存儲屏障的配對使用保障可見性。最后又通過鎖的排他性保障了原子性。
本文總結自:
黃文海《Java多線程編程實戰指南》——第3、12章