JVM併發機制的探討——內存模型、內存可見性和指令重排序

原文出處:  oschina

併發原本就是個有意思的問題,尤爲是如今又流行這麼一句話:「高帥富加機器,窮矮搓搞優化」。從這句話能夠看到,不管是高帥富仍是窮矮搓都須要深刻理解併發編程,高帥富加多了機器,須要協調多臺機器或者多個CPU對共享資源的訪問,所以須要瞭解併發,窮矮搓搞優化須要編寫各類多線程的代碼來壓榨CPU的計算資源,讓它在同一時刻作更多的事情,這個更須要了解併發。html

在我前一篇關於併發的文章http://my.oschina.net/chihz/blog/54731中提到過管程,管程的特點是在編程語言中對併發的細節進行封裝,使程序員能夠直接在語言中就獲得併發的支持,而沒必要本身去處理一些像是控制信號量之類容易出錯且繁瑣的細節問題。一些語言是經過在編譯時解開語法糖的方式去實現管程,但Java在編譯後生成的字節碼層面上對併發仍然是一層封裝,好比syncrhonized塊在編譯以後只是對應了兩條指令:monitorenter和monitorexit。更多的併發細節是在JVM運行時去處理的,而不是編譯。這篇文章主要是針對JVM處理併發的一些細節的探討。java

JAVA內存模型

對於咱們平時開發的業務應用來講,內存應該是訪問速度最快的存儲設備,對於頻繁訪問的數據,咱們老是習慣把它們放到內存緩存中,有句話不是說麼,緩存就像是清涼油,哪裏有問題就抹一抹。可是CPU的運算速度比起內存的訪問速度還要快幾個量級,爲了平衡這個差距,因而就專門爲CPU引入了高速緩存,頻繁使用的數據放到高速緩存當中,CPU在使用這些數據進行運算的時候就沒必要再去訪問內存。可是在多CPU時代卻有一個問題,每一個CPU都擁有本身的高速緩存,內存又是全部CPU共享的公共資源,因而內存此時就成了一個臨界區,若是控制很差各個CPU對內存的併發訪問,那麼就會產生錯誤,出現數據不一致的狀況。爲了不這種狀況,須要採起緩存一致性協議來保證,這類協議有不少,各個硬件平臺和操做系統的實現不盡相同。
 
JVM須要實現跨平臺的支持,它須要有一套本身的同步協議來屏蔽掉各類底層硬件和操做系統的不一樣,所以就引入了Java內存模型。對於Java來講開發者並不須要關心任何硬件細節,所以沒有多核CPU和高速緩存的概念,多核CPU和高速緩存在JVM中對應的是Java語言內置的線程和每一個線程所擁有的獨立內存空間,Java內存模型所規範的也就是數據在線程本身的獨立內存空間和JVM共享內存之間同步的問題。下面這兩張圖說明了硬件平臺和JVM內存模型的類似和差別之處。
 
硬件平臺

 
JVM內存模型
 

Java內存模型規定,對於多個線程共享的變量,存儲在主內存當中,每一個線程都有本身獨立的工做內存,線程只能訪問本身的工做內存,不能夠訪問其它線程的工做內存。工做內存中保存了主內存共享變量的副本,線程要操做這些共享變量,只能經過操做工做內存中的副原本實現,操做完畢以後再同步回到主內存當中。如何保證多個線程操做主內存的數據完整性是一個難題,Java內存模型也規定了工做內存與主內存之間交互的協議,首先是定義了8種原子操做:
(1) lock:將主內存中的變量鎖定,爲一個線程所獨佔
(2) unclock:將lock加的鎖定解除,此時其它的線程能夠有機會訪問此變量
(3) read:將主內存中的變量值讀到工做內存當中
(4) load:將read讀取的值保存到工做內存中的變量副本中。
(5) use:將值傳遞給線程的代碼執行引擎
(6) assign:將執行引擎處理返回的值從新賦值給變量副本
(7) store:將變量副本的值存儲到主內存中。
(8) write:將store存儲的值寫入到主內存的共享變量當中。
 
咱們能夠看到,要保證數據的同步,lock和unlock定義了一個線程訪問一次共享內存的界限,有lock操做也必須有unlock操做,另一些操做也必需要成對出現才能夠,像是read和load、store和write須要成對出現,若是單一指令出現,那麼就會形成數據不一致的問題。Java內存模型也針對這些操做指定了必須知足的規則:
(1) read和load、store和write必需要成對出現,不容許單一的操做,不然會形成從主內存讀取的值,工做內存不接受或者工做內存發起的寫入操做而主內存沒法接受的現象。
(2) 在線程中使用了assign操做改變了變量副本,那麼就必須把這個副本經過store-write同步回主內存中。若是線程中沒有發生assign操做,那麼也不容許使用store-write同步到主內存。
(3) 在對一個變量實行use和store操做以前,必須實行過load和assign操做。
(4) 變量在同一時刻只容許一個線程對其進行lock,有多少次lock操做,就必須有多少次unlock操做。在lock操做以後會清空此變量在工做內存中原先的副本,須要再次從主內存read-load新的值。在執行unlock操做前,須要把改變的副本同步回主存。
 

內存可見性

經過上面Java內存模型的概述,咱們會注意到這麼一個問題,每一個線程在獲取鎖以後會在本身的工做內存來操做共享變量,在工做內存中的副本回寫到主內存,而且其它線程從主內存將變量同步回本身的工做內存以前,共享變量的改變對其它線程是不可見的。那麼不少時候咱們須要一個線程對共享變量的改動,其它線程也須要當即得知這個改動該怎麼辦呢?好比如下的情景,有一個全局的狀態變量open:
1
boolean open= true;
這個變量用來描述對一個資源的打開關閉狀態,true表示打開,false表示關閉,假設有一個線程A,在執行一些操做後將open修改成false:
 
1
2
3
4
//線程A
 
resource.close();
open = false;

 

線程B隨時關注open的狀態,當open爲true的時候經過訪問資源來進行一些操做:
 
1
2
3
4
5
//線程B
 
while(open) {
         doSomethingWithResource(resource);
}

 

當A把資源關閉的時候,open變量對線程B不可見,若是此時open變量的改動還沒有同步到線程B的工做內存中,那麼線程B就會用一個已經關閉了的資源去作一些操做,所以產生錯誤。
 
因此對於上面的情景,要求一個線程對open的改變,其餘的線程可以當即可見,Java爲此提供了volatile關鍵字,在聲明open變量的時候加入volatile關鍵字就能夠保證open的內存可見性,即open的改變對全部的線程都是當即可見的。volatile保證可見性的原理是在每次訪問變量時都會進行一次刷新,所以每次訪問都是主內存中最新的版本。
 

指令重排序

不少介紹JVM併發的書或文章都會談到JVM爲了優化性能,採用了指令重排序,可是對於什麼是指令重排序,爲何重排序會優化性能卻不多有說起,其實道理很簡單,假設有這麼兩個共享變量a和b:
 
1
2
private int a;
private int b;

 

在線程A中有兩條語句對這兩個共享變量進行賦值操做:
1
2
a = 1;
b = 2;

 

假設當線程A對a進行復制操做的時候發現這個變量在主內存已經被其它的線程加了訪問鎖,那麼此時線程A怎麼辦?等待釋放鎖?不,等待太浪費時間了,它會去嘗試進行b的賦值操做,b這時候沒被人佔用,所以就會先爲b賦值,再去爲a賦值,那麼執行的順序就變成了:
1
2
b = 2;
a = 1;

 

對於在同一個線程內,這樣的改變是不會對邏輯產生影響的,可是在多線程的狀況下指令重排序會帶來問題,看下面這個情景:
在線程A中:
1
2
context = loadContext();
inited = true;
在線程B中:
 

1
2
3
4
while(!inited ){
     sleep
}
doSomethingwithconfig(context);
假設A中發生了重排序:
 

1
2
inited = true;
context = loadContext();
那麼B中極可能就會拿到一個還沒有初始化或還沒有初始化完成的context,從而引起程序錯誤。
 
想到有一條古老的原則很適合用在這個地方,那就是先要保證程序的正確而後再去優化性能。此處因爲重排序產生的錯誤顯然要比重排序帶來的性能優化要重要的多。要解決重排序問題仍是經過volatile關鍵字,volatile關鍵字能確保變量在線程中的操做不會被重排序而是按照代碼中規定的順序進行訪問。
 

最後的總結

這篇文章簡單的介紹了Java內存模型、內存可見性和指令重排序。不過最後看來其實主要是在解釋volatile這個關鍵字,我的感受volatile關鍵字是Java當中最使人困惑和最難理解的關鍵字。相對於synchronized塊的代碼鎖,volatile應該是提供了一個輕量級的針對共享變量的鎖,當咱們在多個線程間使用共享變量進行通訊的時候須要考慮將共享變量用volatile來修飾,對於須要使用volatile的各類情景,看到IBM Developer Works上有一篇文章總結的很不錯,推薦一下: http://www.ibm.com/developerworks/cn/java/j-jtp06197.html程序員

 

補充說明:64位long和double

在JVM規範中Java內存模型要求lock、unlock、read、load、assign、use、store、write這8個操做必須是原子的,可是對於64位的long和double來講,若是沒有被volatile修飾符修飾,那麼能夠不是原子的,注意是能夠,即虛擬機在實現的時候能夠選擇是不是原子操做。目前幾乎全部的商用虛擬機都將此實現爲原子操做,所以沒必要每次用到它們都去加volatile修飾。
相關文章
相關標籤/搜索