Java并發(fā)編程(十四)Java內(nèi)存模型
1.共享內(nèi)存和消息傳遞
線程之間的通信機(jī)制有兩種:共享內(nèi)存和消息傳遞;在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),線程之間通過(guò)寫(xiě)-讀內(nèi)存中的公共狀態(tài)來(lái)隱式進(jìn)行通信。在消息傳遞的并發(fā)模型里,線程之間沒(méi)有公共狀態(tài),線程之間必須通過(guò)明確的發(fā)送消息來(lái)顯式進(jìn)行通信。
同步是指程序用于控制不同線程之間操作發(fā)生相對(duì)順序的機(jī)制。在共享內(nèi)存并發(fā)模型里,同步是顯式進(jìn)行的。工程師必須顯式指定某個(gè)方法或某段代碼需要在線程之間互斥執(zhí)行。在消息傳遞的并發(fā)模型里,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式進(jìn)行的。
Java的并發(fā)采用的是共享內(nèi)存模型,Java線程之間的通信總是隱式進(jìn)行,整個(gè)通信過(guò)程對(duì)工程師完全透明。
2.Java內(nèi)存模型的抽象
在java中,所有實(shí)例域、靜態(tài)域和數(shù)組元素存儲(chǔ)在堆內(nèi)存中,堆內(nèi)存在線程之間共享(本文使用“共享變量”這個(gè)術(shù)語(yǔ)代指實(shí)例域,靜態(tài)域和數(shù)組元素)。局部變量,方法定義參數(shù)和異常處理器參數(shù)不會(huì)在線程之間共享,它們不會(huì)有內(nèi)存可見(jiàn)性問(wèn)題,也不受內(nèi)存模型的影響。
Java線程之間的通信由Java內(nèi)存模型(本文簡(jiǎn)稱(chēng)為JMM)控制,JMM決定一個(gè)線程對(duì)共享變量的寫(xiě)入何時(shí)對(duì)另一個(gè)線程可見(jiàn)。從抽象的角度來(lái)看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存,本地內(nèi)存中存儲(chǔ)了該線程以讀/寫(xiě)共享變量的副本。本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存,寫(xiě)緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。Java內(nèi)存模型的抽象示意圖如下:

從上圖來(lái)看,線程A與線程B之間如要通信的話(huà),必須要經(jīng)歷下面2個(gè)步驟:
- 線程A把本地內(nèi)存A中更新過(guò)的共享變量刷新到主內(nèi)存中去。
- 線程B到主內(nèi)存中去讀取線程A之前已更新過(guò)的共享變量。
3.從源代碼到指令序列的重排序
在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序。重排序分三種類(lèi)型:
- 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。
- 指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴(lài)性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
- 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫(xiě)緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
從java源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)分別經(jīng)歷下面三種重排序:

上述的1屬于編譯器重排序,2和3屬于處理器重排序。這些重排序都可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題。對(duì)于編譯器,JMM的編譯器重排序規(guī)則會(huì)禁止特定類(lèi)型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對(duì)于處理器重排序,JMM的處理器重排序規(guī)則會(huì)要求java編譯器在生成指令序列時(shí),插入特定類(lèi)型的內(nèi)存屏障指令,通過(guò)內(nèi)存屏障指令來(lái)禁止特定類(lèi)型的處理器重排序(不是所有的處理器重排序都要禁止)。
JMM屬于語(yǔ)言級(jí)的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺(tái)之上,通過(guò)禁止特定類(lèi)型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見(jiàn)性保證。
4.happens-before簡(jiǎn)介
happens-before是JMM最核心的概念,對(duì)于Java工程師來(lái)說(shuō),理解happens-before是理解JMM的關(guān)鍵。
JMM的設(shè)計(jì)意圖
在設(shè)計(jì)JMM需要考慮兩個(gè)關(guān)鍵因素:
- 工程師對(duì)內(nèi)存模型的使用,希望內(nèi)存模型易于理解和編程,工程師希望基于一個(gè)強(qiáng)內(nèi)存模型來(lái)編寫(xiě)代碼。
- 編譯器和處理器對(duì)內(nèi)存的實(shí)現(xiàn),希望內(nèi)存模型對(duì)他們的束縛越少越好,編譯器和處理器希望實(shí)現(xiàn)一個(gè)弱內(nèi)存模型。
這兩個(gè)因素是互相矛盾的,所以JSR-133專(zhuān)家組設(shè)計(jì)時(shí)需要考慮到一個(gè)好的平衡點(diǎn):一方面為工程師提供足夠強(qiáng)的內(nèi)存可見(jiàn)性,另一方面要對(duì)編譯器和處理器的限制要盡量松些。
我們來(lái)舉了例子:
int a=10; //A int b=20; //B int c=a*b; //C 上面是一個(gè)簡(jiǎn)單的乘法運(yùn)算,并存在3個(gè)happens-before關(guān)系: 1. A happens-before B 2. B happens-before C 3. A happens-before C 這三個(gè)happens-before關(guān)系中,2和3是必須的,但1是不必要的。因此,JMM把happens-before要求禁止的重排序分為兩類(lèi): 1.會(huì)改變程序執(zhí)行結(jié)果的重排序。 2.不會(huì)改變程序執(zhí)行結(jié)果的重排序。 JMM對(duì)這兩種不同性質(zhì)的重排序,采取了不同的策略: 1.對(duì)于會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。 2.對(duì)于不會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM要求編譯器和處理器不做要求,可以允許這種重排序。
happens-before的定義與規(guī)則
JSR-133使用happens-before的概念來(lái)指定兩個(gè)操作之間的執(zhí)行順序,由于這兩個(gè)操作可以在一個(gè)線程內(nèi),也可以在不同的線程之間。因此,JMM可以通過(guò)happens-before關(guān)系向工程師提供跨線程的內(nèi)存可見(jiàn)性保證。
happens-before規(guī)則如下:
1. 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens- before 于該線程中的任意后續(xù)操作。
2. 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)監(jiān)視器鎖的解鎖,happens- before 于隨后對(duì)這個(gè)監(jiān)視器鎖的加鎖。
3. volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫(xiě),happens- before 于任意后續(xù)對(duì)這個(gè)volatile域的讀。
4. 傳遞性:如果A happens- before B,且B happens- before C,那么A happens- before C。
5.順序一致性
順序一致性?xún)?nèi)存模型是一個(gè)理論參考模型,在設(shè)計(jì)的時(shí)候,處理器的內(nèi)存模型和編程語(yǔ)言的內(nèi)存模型都會(huì)以順序一致性?xún)?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è)線程中寫(xiě)一個(gè)變量,在另一個(gè)線程讀同一個(gè)變量,而且寫(xiě)和讀沒(méi)有通過(guò)同步來(lái)排序。
當(dāng)代碼中包含數(shù)據(jù)競(jìng)爭(zhēng)時(shí),程序的執(zhí)行往往產(chǎn)生違反直覺(jué)的結(jié)果。如果一個(gè)多線程程序能正確同步,這個(gè)程序?qū)⑹且粋€(gè)沒(méi)有數(shù)據(jù)競(jìng)爭(zhēng)的程序。
JMM對(duì)正確同步的多線程程序的內(nèi)存一致性做了如下保證:
如果程序是正確同步的,程序的執(zhí)行將具有順序一致性(sequentially consistent),即程序的執(zhí)行結(jié)果與該程序在順序一致性?xún)?nèi)存模型中的執(zhí)行結(jié)果相同。這里的同步是指廣義上的同步,包括對(duì)常用同步原語(yǔ)(synchronized,volatile和final)的正確使用。
順序一致性模型
順序一致性?xún)?nèi)存模型是一個(gè)被計(jì)算機(jī)科學(xué)家理想化了的理論參考模型,它為程序員提供了極強(qiáng)的內(nèi)存可見(jiàn)性保證。順序一致性?xún)?nèi)存模型有兩大特性:
- 一個(gè)線程中的所有操作必須按照程序的順序來(lái)執(zhí)行。
- (不管程序是否同步)所有線程都只能看到一個(gè)單一的操作執(zhí)行順序。在順序一致性?xún)?nèi)存模型中,每個(gè)操作都必須原子執(zhí)行且立刻對(duì)所有線程可見(jiàn)。
順序一致性?xún)?nèi)存模型為程序員提供的視圖如下:

在概念上,順序一致性模型有一個(gè)單一的全局內(nèi)存,這個(gè)內(nèi)存通過(guò)一個(gè)左右擺動(dòng)的開(kāi)關(guān)可以連接到任意一個(gè)線程。同時(shí),每一個(gè)線程必須按程序的順序來(lái)執(zhí)行內(nèi)存讀/寫(xiě)操作。從上圖我們可以看出,在任意時(shí)間點(diǎn)最多只能有一個(gè)線程可以連接到內(nèi)存。當(dāng)多個(gè)線程并發(fā)執(zhí)行時(shí),圖中的開(kāi)關(guān)裝置能把所有線程的所有內(nèi)存讀/寫(xiě)操作串行化。
順序一致性?xún)?nèi)存模型中的每個(gè)操作必須立即對(duì)任意線程可見(jiàn),但是在JMM中就沒(méi)有這個(gè)保證。未同步程序在JMM中不但整體的執(zhí)行順序是無(wú)序的,而且所有線程看到的操作執(zhí)行順序也可能不一致。比如,在當(dāng)前線程把寫(xiě)過(guò)的數(shù)據(jù)緩存在本地內(nèi)存中,且還沒(méi)有刷新到主內(nèi)存之前,這個(gè)寫(xiě)操作僅對(duì)當(dāng)前線程可見(jiàn);從其他線程的角度來(lái)觀察,會(huì)認(rèn)為這個(gè)寫(xiě)操作根本還沒(méi)有被當(dāng)前線程執(zhí)行。只有當(dāng)前線程把本地內(nèi)存中寫(xiě)過(guò)的數(shù)據(jù)刷新到主內(nèi)存之后,這個(gè)寫(xiě)操作才能對(duì)其他線程可見(jiàn)。在這種情況下,當(dāng)前線程和其它線程看到的操作執(zhí)行順序?qū)⒉灰恢隆?/span>
同步程序的順序一致性
我們接下來(lái)看看正確同步的程序如何具有順序一致性。
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)視器的語(yǔ)義)。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根本無(wú)法“觀察”到線程A在臨界區(qū)內(nèi)的重排序。這種重排序既提高了執(zhí)行效率,又沒(méi)有改變程序的執(zhí)行結(jié)果。
從這里我們可以看到JMM在具體實(shí)現(xiàn)上的基本方針:在不改變(正確同步的)程序執(zhí)行結(jié)果的前提下,盡可能的為編譯器和處理器的優(yōu)化打開(kāi)方便之門(mén)。
未同步程序的順序一致性
JMM不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致。因?yàn)槲赐匠绦蛟陧樞蛞恢滦阅P椭袌?zhí)行時(shí),整體上是無(wú)序的,其執(zhí)行結(jié)果無(wú)法預(yù)知。保證未同步程序在兩個(gè)模型中的執(zhí)行結(jié)果一致毫無(wú)意義。
和順序一致性模型一樣,未同步程序在JMM中的執(zhí)行時(shí),整體上也是無(wú)序的,其執(zhí)行結(jié)果也無(wú)法預(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型變量的讀/寫(xiě)操作具有原子性,而順序一致性模型保證對(duì)所有的內(nèi)存讀/寫(xiě)操作都具有原子性。
對(duì)于第三個(gè)差異:在一些32位的處理器上,如果要求對(duì)64位數(shù)據(jù)的讀/寫(xiě)操作具有原子性,會(huì)有比較大的開(kāi)銷(xiāo)。為了照顧這種處理器,java語(yǔ)言規(guī)范鼓勵(lì)但不強(qiáng)求JVM對(duì)64位的long型變量和double型變量的讀/寫(xiě)具有原子性。當(dāng)JVM在這種處理器上運(yùn)行時(shí),會(huì)把一個(gè)64位long/ double型變量的讀/寫(xiě)操作拆分為兩個(gè)32位的讀/寫(xiě)操作來(lái)執(zhí)行。這兩個(gè)32位的讀/寫(xiě)操作可能會(huì)被分配到不同的總線事務(wù)中執(zhí)行,此時(shí)對(duì)這個(gè)64位變量的讀/寫(xiě)將不具有原子性。
當(dāng)單個(gè)內(nèi)存操作不具有原子性,將可能會(huì)產(chǎn)生意想不到后果。請(qǐng)看下面示意圖:
如上圖所示,假設(shè)處理器A寫(xiě)一個(gè)long型變量,同時(shí)處理器B要讀這個(gè)long型變量。處理器A中64位的寫(xiě)操作被拆分為兩個(gè)32位的寫(xiě)操作,且這兩個(gè)32位的寫(xiě)操作被分配到不同的寫(xiě)事務(wù)中執(zhí)行。同時(shí)處理器B中64位的讀操作被拆分為兩個(gè)32位的讀操作,且這兩個(gè)32位的讀操作被分配到同一個(gè)的讀事務(wù)中執(zhí)行。當(dāng)處理器A和B按上圖的時(shí)序來(lái)執(zhí)行時(shí),處理器B將看到僅僅被處理器A“寫(xiě)了一半“的無(wú)效值。
posted on 2016-12-23 01:06 安卓筆記俠 閱讀(380) 評(píng)論(0) 收藏 舉報(bào)
浙公網(wǎng)安備 33010602011771號(hào)