Java并發(fā)編程(四)Java內(nèi)存模型

相關(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è)步驟:

  1. 線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去实蔽。
  2. 線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量荡碾。

3.從源代碼到指令序列的重排序

在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序局装。重排序分三種類型:

  1. 編譯器優(yōu)化的重排序坛吁。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序铐尚。
  2. 指令級(jí)并行的重排序〔β觯現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性宣增,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序玫膀。
  3. 內(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)鍵因素:

  1. 工程師對(duì)內(nèi)存模型的使用茎杂,希望內(nèi)存模型易于理解和編程错览,工程師希望基于一個(gè)強(qiáng)內(nèi)存模型來編寫代碼。
  2. 編譯器和處理器對(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)系:

  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要求禁止的重排序分為兩類:

  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的概念來指定兩個(gè)操作之間的執(zhí)行順序漓踢,由于這兩個(gè)操作可以在一個(gè)線程內(nèi),也可以在不同的線程之間和簸。因此彭雾,JMM可以通過happens-before關(guān)系向工程師提供跨線程的內(nèi)存可見性保證。

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域的寫吴菠,happens- before 于任意后續(xù)對(duì)這個(gè)volatile域的讀。
  4. 傳遞性:如果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)存模型有兩大特性:

  1. 一個(gè)線程中的所有操作必須按照程序的順序來執(zhí)行。
  2. (不管程序是否同步)所有線程都只能看到一個(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è)差異:

  1. 順序一致性模型保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行冯袍,而JMM不保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行(比如上面正確同步的多線程程序在臨界區(qū)內(nèi)的重排序)。
  2. 順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序碾牌,而JMM不保證所有線程能看到一致的操作執(zhí)行順序康愤。
  3. 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ǔ)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末攻锰,一起剝皮案震驚了整個(gè)濱河市晾嘶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌娶吞,老刑警劉巖垒迂,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異妒蛇,居然都是意外死亡机断,警方通過查閱死者的電腦和手機(jī)楷拳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吏奸,“玉大人欢揖,你說我怎么就攤上這事》芪担” “怎么了她混?”我有些...
    開封第一講書人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)泊碑。 經(jīng)常有香客問我坤按,道長(zhǎng),這世上最難降的妖魔是什么馒过? 我笑而不...
    開封第一講書人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任臭脓,我火速辦了婚禮,結(jié)果婚禮上腹忽,老公的妹妹穿的比我還像新娘来累。我一直安慰自己,他們只是感情好窘奏,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開白布嘹锁。 她就那樣靜靜地躺著,像睡著了一般蔼夜。 火紅的嫁衣襯著肌膚如雪兼耀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評(píng)論 1 290
  • 那天求冷,我揣著相機(jī)與錄音瘤运,去河邊找鬼。 笑死匠题,一個(gè)胖子當(dāng)著我的面吹牛拯坟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播韭山,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼郁季,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了钱磅?” 一聲冷哼從身側(cè)響起邑贴,我...
    開封第一講書人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤念恍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體躁染,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡准颓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片答憔。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖掀抹,靈堂內(nèi)的尸體忽然破棺而出虐拓,到底是詐尸還是另有隱情,我是刑警寧澤傲武,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布蓉驹,位于F島的核電站,受9級(jí)特大地震影響谱轨,放射性物質(zhì)發(fā)生泄漏戒幔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一土童、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧工坊,春花似錦献汗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至昭齐,卻和暖如春尿招,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背阱驾。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工就谜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人里覆。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓丧荐,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親喧枷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子虹统,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容