學(xué)習(xí)資料:http://www./cn/articles/java-memory-model-1
Java的并發(fā)采用的是共享內(nèi)存模型(而非消息傳遞模型),,線程之間共享程序的公共狀態(tài),線程之間通過寫-讀內(nèi)存中的公共狀態(tài)來隱式進(jìn)行通信,。多個線程之間是不能直接傳遞數(shù)據(jù)交互的,,它們之間的交互只能通過共享變量來實現(xiàn) 同步是顯式進(jìn)行的。程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執(zhí)行,。
1,、多線程通信
|
名稱 | 代碼示例 | 說明 |
寫后讀 | a = 1;b = a; | 寫一個變量之后,再讀這個位置,。 |
寫后寫 | a = 1;a = 2; | 寫一個變量之后,,再寫這個變量。 |
讀后寫 | a = b;b = 1; | 讀一個變量之后,,再寫這個變量,。 |
上面三種情況,只要重排序兩個操作的執(zhí)行順序,,程序的執(zhí)行結(jié)果將會被改變,。
前面提到過,編譯器和處理器可能會對操作做重排序,。編譯器和處理器在重排序時,,會遵守數(shù)據(jù)依賴性,編譯器和處理器不會改變存在數(shù)據(jù)依賴關(guān)系的兩個操作的執(zhí)行順序,。
注意,,這里所說的數(shù)據(jù)依賴性僅針對單個處理器中執(zhí)行的指令序列和單個線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮,。
as-if-serial語義的意思指:不管怎么重排序(編譯器和處理器為了提高并行度),,(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器,,runtime 和處理器都必須遵守as-if-serial語義,。
【例】
如上圖所示,A和C之間存在數(shù)據(jù)依賴關(guān)系,,同時B和C之間也存在數(shù)據(jù)依賴關(guān)系,。因此在最終執(zhí)行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,,程序的結(jié)果將會被改變),。但A和B之間沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以重排序A和B之間的執(zhí)行順序,。下圖是該程序的兩種執(zhí)行順序:
as-if-serial語義把單線程程序保護(hù)了起來,,遵守as-if-serial語義的編譯器,runtime 和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個幻覺:單線程程序是按程序的順序來執(zhí)行的,。as-if-serial語義使單線程程序員無需擔(dān)心重排序會干擾他們,,也無需擔(dān)心內(nèi)存可見性問題,。
從JDK5開始,java使用新的JSR -133內(nèi)存模型,。JSR-133提出了happens-before的概念,,通過這個概念來闡述操作之間的內(nèi)存可見性。如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,,那么這兩個操作之間必須存在happens-before關(guān)系,。這里提到的兩個操作既可以是在一個線程之內(nèi),也可以是在不同線程之間,。 與程序員密切相關(guān)的happens-before規(guī)則如下:
注意,,兩個操作之間具有happens-before關(guān)系,并不意味著前一個操作必須要在后一個操作之前執(zhí)行,!happens-before僅僅要求前一個操作(執(zhí)行的結(jié)果)對后一個操作可見,,且前一個操作按順序排在第二個操作之前(the first is visible to and ordered before the second)。happens- before的定義很微妙,,后文會具體說明happens-before為什么要這么定義,。
這里的第3個happens- before關(guān)系,,是根據(jù)happens- before的傳遞性推導(dǎo)出來的,。
這里A happens- before B,但實際執(zhí)行時B卻可以排在A之前執(zhí)行(看上面的重排序后的執(zhí)行順序),。A happens- before B,,JMM并不要求A一定要在B之前執(zhí)行。JMM僅僅要求前一個操作(執(zhí)行的結(jié)果)對后一個操作可見,,且前一個操作按順序排在第二個操作之前,。這里操作A的執(zhí)行結(jié)果不需要對操作B可見;而且重排序操作A和操作B后的執(zhí)行結(jié)果,,與操作A和操作B按happens- before順序執(zhí)行的結(jié)果一致,。在這種情況下,,JMM會認(rèn)為這種重排序并不非法(not illegal),JMM允許這種重排序,。
在計算機(jī)中,,軟件技術(shù)和硬件技術(shù)有一個共同的目標(biāo):在不改變程序執(zhí)行結(jié)果的前提下,盡可能的開發(fā)并行度,。編譯器和處理器遵從這一目標(biāo),,從happens- before的定義我們可以看出,JMM同樣遵從這一目標(biāo),。
現(xiàn)在讓我們來看看,重排序是否會改變多線程程序的執(zhí)行結(jié)果,?!纠浚?/p>
flag變量是個標(biāo)記,用來標(biāo)識變量a是否已被寫入,。這里假設(shè)有兩個線程A和B,,A首先執(zhí)行writer()方法,隨后B線程接著執(zhí)行reader()方法,。線程B在執(zhí)行操作4時,,能否看到線程A在操作1對共享變量a的寫入?
答案是:不一定能看到,。
由于操作1和操作2沒有數(shù)據(jù)依賴關(guān)系,,編譯器和處理器可以對這兩個操作重排序;同樣,,操作3和操作4沒有數(shù)據(jù)依賴關(guān)系(,?),編譯器和處理器也可以對這兩個操作重排序,。讓我們先來看看,,當(dāng)操作1和操作2重排序時,可能會產(chǎn)生什么效果,?請看下面的程序執(zhí)行時序圖:
如上圖所示,,操作1和操作2做了重排序。程序執(zhí)行時,,線程A首先寫標(biāo)記變量flag,,隨后線程B讀這個變量。由于條件判斷為真,,線程B將讀取變量a,。此時,變量a還根本沒有被線程A寫入,,在這里多線程程序的語義被重排序破壞了,!
下面再讓我們看看,,當(dāng)操作3和操作4重排序時會產(chǎn)生什么效果(借助這個重排序,可以順便說明控制依賴性),。下面是操作3和操作4重排序后,,程序的執(zhí)行時序圖:
在程序中,操作3和操作4存在控制依賴關(guān)系,。當(dāng)代碼中存在控制依賴性時,,會影響指令序列執(zhí)行的并行度。為此,,編譯器和處理器會采用猜測(Speculation)執(zhí)行來克服控制相關(guān)性對并行度的影響,。以處理器的猜測執(zhí)行為例,執(zhí)行線程B的處理器可以提前讀取并計算a*a,,然后把計算結(jié)果臨時保存到一個名為重排序緩沖(reorder buffer ROB)的硬件緩存中,。當(dāng)接下來操作3的條件判斷為真時,就把該計算結(jié)果寫入變量i中,。
從圖中我們可以看出,,猜測執(zhí)行實質(zhì)上對操作3和4做了重排序。重排序在這里破壞了多線程程序的語義,!
在單線程程序中,,對存在控制依賴的操作重排序,不會改變執(zhí)行結(jié)果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因),;但在多線程程序中,,對存在控制依賴的操作重排序,可能會改變程序的執(zhí)行結(jié)果,。
3,、順序一致性
當(dāng)程序未正確同步時,就會存在數(shù)據(jù)競爭,。java內(nèi)存模型規(guī)范對數(shù)據(jù)競爭的定義如下:
當(dāng)代碼中包含數(shù)據(jù)競爭時,,程序的執(zhí)行往往產(chǎn)生違反直覺的結(jié)果(前一章的示例正是如此)。如果一個多線程程序能正確同步,,這個程序?qū)⑹且粋€沒有數(shù)據(jù)競爭的程序,。
JMM對正確同步的多線程程序的內(nèi)存一致性做了如下保證:
順序一致性內(nèi)存模型有兩大特性:
順序一致性內(nèi)存模型為程序員提供的視圖如下,。在概念上,順序一致性模型有一個單一的全局內(nèi)存,,這個內(nèi)存通過一個左右擺動的開關(guān)可以連接到任意一個線程,。同時,每一個線程必須按程序的順序來執(zhí)行內(nèi)存讀/寫操作,。在任意時間點(diǎn)最多只能有一個線程可以連接到內(nèi)存,。當(dāng)多個線程并發(fā)執(zhí)行時,圖中的開關(guān)裝置能把所有線程的所有內(nèi)存讀/寫操作串行化,。
為了更好的理解,,下面我們通過兩個示意圖來對順序一致性模型的特性做進(jìn)一步的說明。
假設(shè)有兩個線程A和B并發(fā)執(zhí)行,。其中A線程有三個操作,它們在程序中的順序是:A1->A2->A3,。B線程也有三個操作,,它們在程序中的順序是:B1->B2->B3。
假設(shè)這兩個線程使用監(jiān)視器來正確同步:A線程的三個操作執(zhí)行后釋放監(jiān)視器,,隨后B線程獲取同一個監(jiān)視器,。那么程序在順序一致性模型中的執(zhí)行效果將如下圖所示:
假設(shè)這兩個線程沒有做同步,下面是這個未同步程序在順序一致性模型中的執(zhí)行示意圖:
未同步程序在順序一致性模型中雖然整體執(zhí)行順序是無序的,,但所有線程都只能看到一個一致的整體執(zhí)行順序,。以上圖為例,線程A和B看到的執(zhí)行順序都是:B1->A1->A2->B2->A3->B3,。之所以能得到這個保證是因為順序一致性內(nèi)存模型中的每個操作必須立即對任意線程可見,。
但是,在JMM中就沒有這個保證,。未同步程序在JMM中不但整體的執(zhí)行順序是無序的,,而且所有線程看到的操作執(zhí)行順序也可能不一致。比如,,在當(dāng)前線程把寫過的數(shù)據(jù)緩存在本地內(nèi)存中,,且還沒有刷新到主內(nèi)存之前,這個寫操作僅對當(dāng)前線程可見,;從其他線程的角度來觀察,,會認(rèn)為這個寫操作根本還沒有被當(dāng)前線程執(zhí)行。只有當(dāng)前線程把本地內(nèi)存中寫過的數(shù)據(jù)刷新到主內(nèi)存之后,,這個寫操作才能對其他線程可見,。在這種情況下,,當(dāng)前線程和其它線程看到的操作執(zhí)行順序?qū)⒉灰恢隆?/p>
在順序一致性模型中,所有操作完全按程序的順序串行執(zhí)行,。而在JMM中,,臨界區(qū)內(nèi)的代碼可以重排序。
對于未同步或未正確同步的多線程程序,,JMM只提供最小安全性:線程執(zhí)行時讀取到的值,,要么是之前某個線程寫入的值,要么是默認(rèn)值(0,,null,,false),JMM保證線程讀操作讀取到的值不會無中生有(out of thin air)的冒出來,。
為了實現(xiàn)最小安全性,,JVM在堆上分配對象時,首先會清零內(nèi)存空間,,然后才會在上面分配對象(JVM內(nèi)部會同步這兩個操作),。因此,在以清零的內(nèi)存空間(pre-zeroed memory)分配對象時,,域的默認(rèn)初始化已經(jīng)完成了,。
JMM不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致。因為未同步程序在順序一致性模型中執(zhí)行時,,整體上是無序的,,其執(zhí)行結(jié)果無法預(yù)知。保證未同步程序在兩個模型中的執(zhí)行結(jié)果一致毫無意義,。
和順序一致性模型一樣,,未同步程序在JMM中的執(zhí)行時,整體上也是無序的,,其執(zhí)行結(jié)果也無法預(yù)知,。同時,未同步程序在這兩個模型中的執(zhí)行特性有下面幾個差異:
第三點(diǎn)差異與處理器總線的工作機(jī)制密切相關(guān)。在計算機(jī)中,數(shù)據(jù)通過總線在處理器和內(nèi)存之間傳遞,。每次處理器和內(nèi)存之間的數(shù)據(jù)傳遞都是通過一系列步驟來完成的,,這一系列步驟稱之為總線事務(wù)(bus transaction)??偩€事務(wù)包括讀事務(wù)(read transaction)和寫事務(wù)(write transaction),。讀事務(wù)從內(nèi)存?zhèn)魉蛿?shù)據(jù)到處理器,寫事務(wù)從處理器傳送數(shù)據(jù)到內(nèi)存,,每個事務(wù)會讀/寫內(nèi)存中一個或多個物理上連續(xù)的字,。這里的關(guān)鍵是,總線會同步試圖并發(fā)使用總線的事務(wù),。在一個處理器執(zhí)行總線事務(wù)期間,,總線會禁止其它所有的處理器和I/O設(shè)備執(zhí)行內(nèi)存的讀/寫。
在一些32位的處理器上,,如果要求對64位數(shù)據(jù)的讀/寫操作具有原子性,,會有比較大的開銷。為了照顧這種處理器,,java語言規(guī)范鼓勵但不強(qiáng)求JVM對64位的long型變量和double型變量的讀/寫具有原子性,。當(dāng)JVM在這種處理器上運(yùn)行時,會把一個64位long/ double型變量的讀/寫操作拆分為兩個32位的讀/寫操作來執(zhí)行,。這兩個32位的讀/寫操作可能會被分配到不同的總線事務(wù)中執(zhí)行,,此時對這個64位變量的讀/寫將不具有原子性。
當(dāng)單個內(nèi)存操作不具有原子性,,將可能會產(chǎn)生意想不到后果。請看下面示意圖:
如上圖所示,,假設(shè)處理器A寫一個long型變量,,同時處理器B要讀這個long型變量。處理器A中64位的寫操作被拆分為兩個32位的寫操作,,且這兩個32位的寫操作被分配到不同的寫事務(wù)中執(zhí)行,。同時處理器B中64位的讀操作被拆分為兩個32位的讀操作,且這兩個32位的讀操作被分配到同一個的讀事務(wù)中執(zhí)行,。當(dāng)處理器A和B按上圖的時序來執(zhí)行時,,處理器B將看到僅僅被處理器A“寫了一半“的無效值。
把對volatile變量的單個讀/寫,,看成是使用同一個監(jiān)視器鎖對這些單個讀/寫操作做了同步。對一個volatile變量的讀,,總是能看到(任意線程)對這個volatile變量最后的寫入,。
這意味著即使是64位的long型和double型變量,只要它是volatile變量,對該變量的讀寫就將具有原子性,。如果是多個volatile操作或類似于volatile 這種復(fù)合操作,,這些操作整體上不具有原子性。
簡而言之,,volatile變量自身具有下列特性:
從JSR-133開始,,volatile變量的寫-讀可以實現(xiàn)線程之間的通信,。
從內(nèi)存語義的角度來說,volatile與監(jiān)視器鎖有相同的效果:volatile寫和監(jiān)視器的釋放有相同的內(nèi)存語義,;volatile讀與監(jiān)視器的獲取有相同的內(nèi)存語義,。
假設(shè)線程A執(zhí)行writer()方法之后,線程B執(zhí)行reader()方法,。根據(jù)happens before規(guī)則,,這個過程建立的happens before 關(guān)系可以分為兩類:
上圖中,每一個箭頭鏈接的兩個節(jié)點(diǎn),,代表了一個happens before 關(guān)系,。黑色箭頭表示程序順序規(guī)則;橙色箭頭表示volatile規(guī)則,;藍(lán)色箭頭表示組合這些規(guī)則后提供的happens before保證,。
這里A線程寫一個volatile變量后,B線程讀同一個volatile變量,。A線程在寫volatile變量之前所有可見的共享變量,,在B線程讀同一個volatile變量后,將立即變得對B線程可見,。
volatile寫的內(nèi)存語義如下:
以上面示例程序VolatileExample為例,,假設(shè)線程A首先執(zhí)行writer()方法,,隨后線程B執(zhí)行reader()方法,初始時兩個線程的本地內(nèi)存中的flag和a都是初始狀態(tài)。
下圖是線程A執(zhí)行volatile寫后,,共享變量的狀態(tài)示意圖,。線程A在寫flag變量后,本地內(nèi)存A中被線程A更新過的兩個共享變量的值被刷新到主內(nèi)存中,。此時,,本地內(nèi)存A和主內(nèi)存中的共享變量的值是一致的。
volatile讀的內(nèi)存語義如下:
下面是線程B讀同一個volatile變量后,,共享變量的狀態(tài)示意圖,。在讀flag變量后,本地內(nèi)存B已經(jīng)被置為無效,。此時,,線程B必須從主內(nèi)存中讀取共享變量。線程B的讀取操作將導(dǎo)致本地內(nèi)存B與主內(nèi)存中的共享變量的值也變成一致的了,。
把volatile寫和volatile讀這兩個步驟綜合起來看的話,,在讀線程B讀一個volatile變量后,寫線程A在寫這個volatile變量之前所有可見的共享變量的值都將立即變得對讀線程B可見,。
下面對volatile寫和volatile讀的內(nèi)存語義做個總結(jié):
為了實現(xiàn)volatile內(nèi)存語義,JMM會分別限制編譯器重排序和處理器重排序,。下面是JMM針對編譯器制定的volatile重排序規(guī)則表:
是否能重排序 | 第二個操作 | ||
第一個操作 | 普通讀/寫 | volatile讀 | volatile寫 |
普通讀/寫 | NO | ||
volatile讀 | NO | NO | NO |
volatile寫 | NO | NO |
舉例來說,第三行最后一個單元格的意思是:在程序順序中,,當(dāng)?shù)谝粋€操作為普通變量的讀或?qū)憰r,,如果第二個操作為volatile寫,則編譯器不能重排序這兩個操作,。
從上表我們可以看出:
|
來自: 小新丸子rdyfps > 《Java》