相關(guān)文章
Java并發(fā)編程(一)線程定義乖寒、狀態(tài)和屬性
Java并發(fā)編程(二)同步
Java并發(fā)編程(三)volatile域
前言
此前我們講到了線程玖瘸、同步以及volatile關(guān)鍵字滔蝉,對(duì)于Java的并發(fā)編程我們有必要了解下Java的內(nèi)存模型搭伤,因?yàn)镴ava線程之間的通信對(duì)于工程師來言是完全透明的寸五,內(nèi)存可見性問題很容易使工程師們覺得困惑踩叭,這篇文章我們來主要的講下Java內(nèi)存模型的相關(guān)概念。
1.共享內(nèi)存和消息傳遞
線程之間的通信機(jī)制有兩種:共享內(nèi)存和消息傳遞公罕;在共享內(nèi)存的并發(fā)模型里器紧,線程之間共享程序的公共狀態(tài),線程之間通過寫-讀內(nèi)存中的公共狀態(tài)來隱式進(jìn)行通信楼眷。在消息傳遞的并發(fā)模型里铲汪,線程之間沒有公共狀態(tài),線程之間必須通過明確的發(fā)送消息來顯式進(jìn)行通信罐柳。
同步是指程序用于控制不同線程之間操作發(fā)生相對(duì)順序的機(jī)制掌腰。在共享內(nèi)存并發(fā)模型里,同步是顯式進(jìn)行的硝清。工程師必須顯式指定某個(gè)方法或某段代碼需要在線程之間互斥執(zhí)行辅斟。在消息傳遞的并發(fā)模型里,由于消息的發(fā)送必須在消息的接收之前芦拿,因此同步是隱式進(jìn)行的。
Java的并發(fā)采用的是共享內(nèi)存模型查邢,Java線程之間的通信總是隱式進(jìn)行蔗崎,整個(gè)通信過程對(duì)工程師完全透明。
2.Java內(nèi)存模型的抽象
在java中扰藕,所有實(shí)例域缓苛、靜態(tài)域和數(shù)組元素存儲(chǔ)在堆內(nèi)存中,堆內(nèi)存在線程之間共享(本文使用“共享變量”這個(gè)術(shù)語代指實(shí)例域,靜態(tài)域和數(shù)組元素)未桥。局部變量笔刹,方法定義參數(shù)和異常處理器參數(shù)不會(huì)在線程之間共享,它們不會(huì)有內(nèi)存可見性問題冬耿,也不受內(nèi)存模型的影響舌菜。
Java線程之間的通信由Java內(nèi)存模型(本文簡(jiǎn)稱為JMM)控制,JMM決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見亦镶。從抽象的角度來看日月,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存缤骨,本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的副本爱咬。本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在绊起。它涵蓋了緩存精拟,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化虱歪。Java內(nèi)存模型的抽象示意圖如下:
從上圖來看串前,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個(gè)步驟:
- 線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去实蔽。
- 線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量荡碾。
3.從源代碼到指令序列的重排序
在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序局装。重排序分三種類型:
- 編譯器優(yōu)化的重排序坛吁。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序铐尚。
- 指令級(jí)并行的重排序〔β觯現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性宣增,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序玫膀。
- 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū)爹脾,這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行帖旨。
從java源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)分別經(jīng)歷下面三種重排序:
上述的1屬于編譯器重排序灵妨,2和3屬于處理器重排序解阅。這些重排序都可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題。對(duì)于編譯器泌霍,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)货抄。對(duì)于處理器重排序,JMM的處理器重排序規(guī)則會(huì)要求java編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障指令蟹地,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)积暖。
JMM屬于語言級(jí)的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺(tái)之上怪与,通過禁止特定類型的編譯器重排序和處理器重排序夺刑,為程序員提供一致的內(nèi)存可見性保證。
4.happens-before簡(jiǎn)介
happens-before是JMM最核心的概念琼梆,對(duì)于Java工程師來說性誉,理解happens-before是理解JMM的關(guān)鍵。
JMM的設(shè)計(jì)意圖
在設(shè)計(jì)JMM需要考慮兩個(gè)關(guān)鍵因素:
- 工程師對(duì)內(nèi)存模型的使用茎杂,希望內(nèi)存模型易于理解和編程错览,工程師希望基于一個(gè)強(qiáng)內(nèi)存模型來編寫代碼。
- 編譯器和處理器對(duì)內(nèi)存的實(shí)現(xiàn)煌往,希望內(nèi)存模型對(duì)他們的束縛越少越好倾哺,編譯器和處理器希望實(shí)現(xiàn)一個(gè)弱內(nèi)存模型。
這兩個(gè)因素是互相矛盾的刽脖,所以JSR-133專家組設(shè)計(jì)時(shí)需要考慮到一個(gè)好的平衡點(diǎn):一方面為工程師提供足夠強(qiáng)的內(nèi)存可見性羞海,另一方面要對(duì)編譯器和處理器的限制要盡量松些。
我們來舉了例子:
int a=10; //A
int b=20; //B
int c=a*b; //C
上面是一個(gè)簡(jiǎn)單的乘法運(yùn)算曲管,并存在3個(gè)happens-before關(guān)系:
- A happens-before B
- B happens-before C
- A happens-before C
這三個(gè)happens-before關(guān)系中却邓,2和3是必須的,但1是不必要的院水。因此腊徙,JMM把happens-before要求禁止的重排序分為兩類:
- 會(huì)改變程序執(zhí)行結(jié)果的重排序。
- 不會(huì)改變程序執(zhí)行結(jié)果的重排序檬某。
JMM對(duì)這兩種不同性質(zhì)的重排序撬腾,采取了不同的策略:
- 對(duì)于會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM要求編譯器和處理器必須禁止這種重排序恢恼。
- 對(duì)于不會(huì)改變程序執(zhí)行結(jié)果的重排序民傻,JMM要求編譯器和處理器不做要求,可以允許這種重排序场斑。
happens-before的定義與規(guī)則
JSR-133使用happens-before的概念來指定兩個(gè)操作之間的執(zhí)行順序漓踢,由于這兩個(gè)操作可以在一個(gè)線程內(nèi),也可以在不同的線程之間和簸。因此彭雾,JMM可以通過happens-before關(guān)系向工程師提供跨線程的內(nèi)存可見性保證。
happens-before規(guī)則如下:
- 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作锁保,happens- before 于該線程中的任意后續(xù)操作。
- 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)監(jiān)視器鎖的解鎖,happens- before 于隨后對(duì)這個(gè)監(jiān)視器鎖的加鎖爽柒。
- volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫吴菠,happens- before 于任意后續(xù)對(duì)這個(gè)volatile域的讀。
- 傳遞性:如果A happens- before B浩村,且B happens- before C做葵,那么A happens- before
C。
5.順序一致性
順序一致性內(nèi)存模型是一個(gè)理論參考模型心墅,在設(shè)計(jì)的時(shí)候酿矢,處理器的內(nèi)存模型和編程語言的內(nèi)存模型都會(huì)以順序一致性內(nèi)存模型為參考。
數(shù)據(jù)競(jìng)爭(zhēng)與順序一致性
當(dāng)程序未正確同步時(shí)怎燥,就會(huì)存在數(shù)據(jù)競(jìng)爭(zhēng)瘫筐。數(shù)據(jù)競(jìng)爭(zhēng)指的是:在一個(gè)線程中寫一個(gè)變量,在另一個(gè)線程讀同一個(gè)變量铐姚,而且寫和讀沒有通過同步來排序策肝。
當(dāng)代碼中包含數(shù)據(jù)競(jìng)爭(zhēng)時(shí),程序的執(zhí)行往往產(chǎn)生違反直覺的結(jié)果隐绵。如果一個(gè)多線程程序能正確同步之众,這個(gè)程序?qū)⑹且粋€(gè)沒有數(shù)據(jù)競(jìng)爭(zhēng)的程序。
JMM對(duì)正確同步的多線程程序的內(nèi)存一致性做了如下保證:
如果程序是正確同步的依许,程序的執(zhí)行將具有順序一致性(sequentially consistent)棺禾,即程序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果相同。這里的同步是指廣義上的同步峭跳,包括對(duì)常用同步原語(synchronized膘婶,volatile和final)的正確使用。
順序一致性模型
順序一致性內(nèi)存模型是一個(gè)被計(jì)算機(jī)科學(xué)家理想化了的理論參考模型坦康,它為程序員提供了極強(qiáng)的內(nèi)存可見性保證竣付。順序一致性內(nèi)存模型有兩大特性:
- 一個(gè)線程中的所有操作必須按照程序的順序來執(zhí)行。
- (不管程序是否同步)所有線程都只能看到一個(gè)單一的操作執(zhí)行順序滞欠。在順序一致性內(nèi)存模型中古胆,每個(gè)操作都必須原子執(zhí)行且立刻對(duì)所有線程可見。
順序一致性內(nèi)存模型為程序員提供的視圖如下:
在概念上筛璧,順序一致性模型有一個(gè)單一的全局內(nèi)存逸绎,這個(gè)內(nèi)存通過一個(gè)左右擺動(dòng)的開關(guān)可以連接到任意一個(gè)線程。同時(shí)夭谤,每一個(gè)線程必須按程序的順序來執(zhí)行內(nèi)存讀/寫操作棺牧。從上圖我們可以看出,在任意時(shí)間點(diǎn)最多只能有一個(gè)線程可以連接到內(nèi)存朗儒。當(dāng)多個(gè)線程并發(fā)執(zhí)行時(shí)颊乘,圖中的開關(guān)裝置能把所有線程的所有內(nèi)存讀/寫操作串行化参淹。
順序一致性內(nèi)存模型中的每個(gè)操作必須立即對(duì)任意線程可見,但是在JMM中就沒有這個(gè)保證乏悄。未同步程序在JMM中不但整體的執(zhí)行順序是無序的浙值,而且所有線程看到的操作執(zhí)行順序也可能不一致。比如檩小,在當(dāng)前線程把寫過的數(shù)據(jù)緩存在本地內(nèi)存中开呐,且還沒有刷新到主內(nèi)存之前,這個(gè)寫操作僅對(duì)當(dāng)前線程可見规求;從其他線程的角度來觀察筐付,會(huì)認(rèn)為這個(gè)寫操作根本還沒有被當(dāng)前線程執(zhí)行。只有當(dāng)前線程把本地內(nèi)存中寫過的數(shù)據(jù)刷新到主內(nèi)存之后阻肿,這個(gè)寫操作才能對(duì)其他線程可見瓦戚。在這種情況下,當(dāng)前線程和其它線程看到的操作執(zhí)行順序?qū)⒉灰恢隆?/p>
同步程序的順序一致性
我們接下來看看正確同步的程序如何具有順序一致性冕茅。
class SynchronizedExample {
int a = 0;
boolean flag = false;
public synchronized void writer() {
a = 1;
flag = true;
}
public synchronized void reader() {
if (flag) {
int i = a;
……
}
}
}
上面示例代碼中伤极,假設(shè)A線程執(zhí)行writer()方法后,B線程執(zhí)行reader()方法姨伤。這是一個(gè)正確同步的多線程程序哨坪。根據(jù)JMM規(guī)范,該程序的執(zhí)行結(jié)果將與該程序在順序一致性模型中的執(zhí)行結(jié)果相同乍楚。下面是該程序在兩個(gè)內(nèi)存模型中的執(zhí)行時(shí)序?qū)Ρ葓D:
在順序一致性模型中当编,所有操作完全按程序的順序串行執(zhí)行。而在JMM中徒溪,臨界區(qū)內(nèi)的代碼可以重排序(但JMM不允許臨界區(qū)內(nèi)的代碼“逸出”到臨界區(qū)之外忿偷,那樣會(huì)破壞監(jiān)視器的語義)。JMM會(huì)在退出監(jiān)視器和進(jìn)入監(jiān)視器這兩個(gè)關(guān)鍵時(shí)間點(diǎn)做一些特別處理臊泌,使得線程在這兩個(gè)時(shí)間點(diǎn)具有與順序一致性模型相同的內(nèi)存視圖鲤桥。雖然線程A在臨界區(qū)內(nèi)做了重排序,但由于監(jiān)視器的互斥執(zhí)行的特性渠概,這里的線程B根本無法“觀察”到線程A在臨界區(qū)內(nèi)的重排序茶凳。這種重排序既提高了執(zhí)行效率,又沒有改變程序的執(zhí)行結(jié)果播揪。
從這里我們可以看到JMM在具體實(shí)現(xiàn)上的基本方針:在不改變(正確同步的)程序執(zhí)行結(jié)果的前提下贮喧,盡可能的為編譯器和處理器的優(yōu)化打開方便之門。
未同步程序的順序一致性
JMM不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致猪狈。因?yàn)槲赐匠绦蛟陧樞蛞恢滦阅P椭袌?zhí)行時(shí)箱沦,整體上是無序的,其執(zhí)行結(jié)果無法預(yù)知雇庙。保證未同步程序在兩個(gè)模型中的執(zhí)行結(jié)果一致毫無意義谓形。
和順序一致性模型一樣灶伊,未同步程序在JMM中的執(zhí)行時(shí),整體上也是無序的套耕,其執(zhí)行結(jié)果也無法預(yù)知谁帕。
同時(shí)峡继,未同步程序在這兩個(gè)模型中的執(zhí)行特性有下面幾個(gè)差異:
- 順序一致性模型保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行冯袍,而JMM不保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行(比如上面正確同步的多線程程序在臨界區(qū)內(nèi)的重排序)。
- 順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序碾牌,而JMM不保證所有線程能看到一致的操作執(zhí)行順序康愤。
- JMM不保證對(duì)64位的long型和double型變量的讀/寫操作具有原子性,而順序一致性模型保證對(duì)所有的內(nèi)存讀/寫操作都具有原子性舶吗。
對(duì)于第三個(gè)差異:在一些32位的處理器上征冷,如果要求對(duì)64位數(shù)據(jù)的讀/寫操作具有原子性,會(huì)有比較大的開銷誓琼。為了照顧這種處理器检激,java語言規(guī)范鼓勵(lì)但不強(qiáng)求JVM對(duì)64位的long型變量和double型變量的讀/寫具有原子性。當(dāng)JVM在這種處理器上運(yùn)行時(shí)腹侣,會(huì)把一個(gè)64位long/ double型變量的讀/寫操作拆分為兩個(gè)32位的讀/寫操作來執(zhí)行叔收。這兩個(gè)32位的讀/寫操作可能會(huì)被分配到不同的總線事務(wù)中執(zhí)行,此時(shí)對(duì)這個(gè)64位變量的讀/寫將不具有原子性傲隶。
當(dāng)單個(gè)內(nèi)存操作不具有原子性饺律,將可能會(huì)產(chǎn)生意想不到后果。請(qǐng)看下面示意圖:
如上圖所示跺株,假設(shè)處理器A寫一個(gè)long型變量复濒,同時(shí)處理器B要讀這個(gè)long型變量。處理器A中64位的寫操作被拆分為兩個(gè)32位的寫操作乒省,且這兩個(gè)32位的寫操作被分配到不同的寫事務(wù)中執(zhí)行巧颈。同時(shí)處理器B中64位的讀操作被拆分為兩個(gè)32位的讀操作,且這兩個(gè)32位的讀操作被分配到同一個(gè)的讀事務(wù)中執(zhí)行袖扛。當(dāng)處理器A和B按上圖的時(shí)序來執(zhí)行時(shí)砸泛,處理器B將看到僅僅被處理器A“寫了一半“的無效值。
參考資料:
《Java并發(fā)編程的藝術(shù)》
深入理解Java內(nèi)存模型(一)——基礎(chǔ)