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

1. 并發(fā)編程

1.1 并發(fā)編程的挑戰(zhàn)

并發(fā)編程的目的是為了加快程序的運(yùn)行速度, 但受限于上下文切換和死鎖等問題, 啟動(dòng)更多的線程并非能讓程序最大限度地并行執(zhí)行.

1.1.1 上下文切換

  • CPU 通過分配時(shí)間片的方式來支持多線程.
    • 通過時(shí)間片分配算法來循環(huán)執(zhí)行任務(wù), 任務(wù)切換前會(huì)保存上一個(gè)任務(wù)的狀態(tài), 以便以后再次運(yùn)行時(shí)的再加載.
    • 時(shí)間片一般為幾十毫秒(ms), 所以感覺多個(gè)線程是同時(shí)運(yùn)行的.
    • 上下文切換(Context Switch) : 任務(wù)從保存到再加載的過程.
  • 并發(fā)執(zhí)行會(huì)產(chǎn)生線程創(chuàng)建和上下文切換的開銷, 所以并非一定比串行執(zhí)行更快.
  • 減少上下文切換
    • 無鎖并發(fā)編程. 多線程競(jìng)爭(zhēng)鎖時(shí), 會(huì)引起上下文切換.
    • CAS 算法. 使用CAS 可以在無需加鎖的情況下, 進(jìn)行Atomic 原子操作.
    • 使用最少線程, 避免創(chuàng)建不需要的線程.
    • 協(xié)程. 在單線程中實(shí)現(xiàn)多任務(wù)的調(diào)度和切換.

1.1.2 死鎖

  • 一旦產(chǎn)生死鎖, 就會(huì)造成系統(tǒng)功能不可用.
    • 此時(shí), 業(yè)務(wù)是可感知的, 因?yàn)椴荒芾^續(xù)提供服務(wù)了.
    • 通過dump 線程來查看到底是那個(gè)線程出現(xiàn)了問題.
  • 避免死鎖的常用手段:
    • 避免一個(gè)線程同時(shí)獲取多個(gè)鎖.
    • 避免一個(gè)線程在持有鎖期間同時(shí)占用多個(gè)資源, 盡量保證每個(gè)鎖只占用一個(gè)資源.
    • 嘗試使用定時(shí)鎖. 例如 lock.tryLock(timeout).

1.2 資源限制的挑戰(zhàn)

只有在串行執(zhí)行會(huì)浪費(fèi)資源時(shí), 將其修改為并行執(zhí)行才能加快運(yùn)行速度.

  • 在并發(fā)編程時(shí), 程序的執(zhí)行速度受限于PC 硬件資源或軟件資源, 如網(wǎng)速, 硬盤讀寫速度, CPU 處理速度, 數(shù)據(jù)庫的鏈接數(shù)和socket 連接數(shù)等.
  • 資源限制引發(fā)的問題:
    • 在資源受限時(shí), 將串行執(zhí)行的代碼并發(fā)執(zhí)行, 其結(jié)果仍然是串行執(zhí)行. 由于增加了上下文切換和資源調(diào)度的消耗, 可能會(huì)使得程序的運(yùn)行速度更慢.
    • 例如, 在較慢的網(wǎng)絡(luò)條件下, 下載一個(gè)大文件, 單線程比并發(fā)編程速度更快.

2. Java 內(nèi)存模型(JMM)

現(xiàn)代軟硬件的共同目標(biāo): 在不改變程序執(zhí)行結(jié)果的前提下, 盡可能提高并行度.

2.1 內(nèi)存模型的基礎(chǔ)

  • 并發(fā)編程模型的兩個(gè)關(guān)鍵問題: 線程間如何通信, 以及線程之間如何同步.
    • 通信: 交換信息的機(jī)制. 有兩種常見的方式:
      • 共享內(nèi)存: 讀寫內(nèi)存中的公共狀態(tài)來進(jìn)行隱式通信.
      • 消息傳遞: 無公共狀態(tài), 通過發(fā)送消息來顯示進(jìn)行通信.
    • 同步: 用以控制不同線程間操作發(fā)生的相對(duì)順序的機(jī)制.
      • 共享內(nèi)存的通信機(jī)制下, 必須進(jìn)行顯式的同步.
      • 消息傳遞的通信機(jī)制下, 消息的發(fā)送順序隱式進(jìn)行了同步.
    • JAVA 采用共享內(nèi)存模型, 線程間的通信過程對(duì)外完全透明.

2.2 JMM 的抽象結(jié)構(gòu)

  • JAVA 中的內(nèi)存存儲(chǔ).
    • 堆內(nèi)存: 存放實(shí)例域, 靜態(tài)域, 數(shù)組元素. 在線程間共享.
    • 棧內(nèi)存: 存放局部變量, 方法定義參數(shù)和異常處理器參數(shù).
      • 不會(huì)共享, 也不會(huì)有內(nèi)存可見性問題. 不受JMM 的影響.


  • JMM 決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見.
    • 定義了線程的本地內(nèi)存和主內(nèi)存之間的抽象關(guān)系.
    • 主內(nèi)存負(fù)責(zé)存儲(chǔ)共享變量.
    • 本地內(nèi)存涵蓋了緩存,寫緩存,寄存器及其他優(yōu)化. 它會(huì)存儲(chǔ)該線程讀寫共享變量的副本.
    • 兩個(gè)線程間的通信過程, 必須經(jīng)過主內(nèi)存.

2.3 處理器和內(nèi)存的交互, 內(nèi)存屏障(Memory Barriers / Memory Fence)

  • CPU 會(huì)使用寫緩存區(qū)來臨時(shí)保存需要向內(nèi)存寫入的數(shù)據(jù).
    • 避免由于處理器停頓下來等待向內(nèi)存寫入數(shù)據(jù)而產(chǎn)生的延遲, 從而保證了指令流水線持續(xù)運(yùn)行.
    • 同時(shí), 通過以批處理的方式刷新寫緩存, 以及合并對(duì)同一內(nèi)存地址的多次寫, 減少了對(duì)內(nèi)存總線的占用.
  • 但是, 每個(gè)CPU 上的寫緩存區(qū), 僅對(duì)該CPU 可見.
    • 該特性會(huì)對(duì)內(nèi)存操作的執(zhí)行順序產(chǎn)生影響: CPU 對(duì)內(nèi)存的讀寫操作的執(zhí)行順序, 不一定與內(nèi)存實(shí)際發(fā)生的讀寫順序一致.
    • 由此, CPU 允許對(duì)寫-讀操作進(jìn)行重排序(因?yàn)楸緛硪矡o法保證其順序性, 且重排序能夠提升性能).
  • 在適當(dāng)?shù)奈恢貌迦雰?nèi)存屏障來禁止特定類型的CPU 重排序.
屏障類型 指令示例
LoadLoad Barries Load1; LoadLoad; Load2
StoreStore Barries Store1; StoreStore; Store2
Load?Store Barries Load1; LoadStore; Store2
StoreLoad Barries Store1; StoreLoad; Load2
  • 其中, StoreLoad 同時(shí)具有其它3個(gè)屏障的效果, 執(zhí)行它的開銷很大, 因?yàn)樗枰褜懢彺鎱^(qū)的數(shù)據(jù)全部刷新到內(nèi)存中(Buffer Fully Flush).

2.4 happens-before

2.4.1 happens-before 的定義

  • 闡述操作之間的內(nèi)存可見性:
    • 如果一個(gè)操作的結(jié)果需要對(duì)另一個(gè)操作可見, 那么兩個(gè)操作之間必須要存在happends-before 關(guān)系.
      • 兩個(gè)操作可以是一個(gè)線程內(nèi), 或者在不同的線程間.
    • 存在happens-before 關(guān)系, 并不意味著Java 平臺(tái)的具體實(shí)現(xiàn)必須要按照關(guān)系指定的順序來執(zhí)行.
      • 重排序后的執(zhí)行結(jié)果與按happens-before 關(guān)系執(zhí)行的結(jié)果一致時(shí), 該重排序是合法的.
  • 目的: 在不改變程序結(jié)果的前提下, 盡可能提高程序執(zhí)行的并行度.
    • as-if-serial: 保證單線程內(nèi)程序執(zhí)行結(jié)果不被改變, happens-before 保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變.

2.4.2 happens-before 規(guī)則:

  • 程序順序規(guī)則: 一個(gè)線程中的每個(gè)操作, happens-before 于該線程中的任意后續(xù)操作.
  • 監(jiān)視器鎖規(guī)則: 對(duì)一個(gè)鎖的解鎖, happens-before 于隨后對(duì)該鎖的加鎖.
  • volatile 變量規(guī)則: 對(duì)一個(gè)volatile 域的寫, happens-before 于任意后續(xù)對(duì)這個(gè)volatile 域的讀.
  • 傳遞性: A happens-before B, B happens-before C, 則A happens-before C.
  • start() 規(guī)則: 線程A 執(zhí)行ThreadB.start(), 則該操作happens-before 于線程B 中的任意操作.
  • join() 規(guī)則: 如果線程A 執(zhí)行ThreadB.join() 并成功返回, 則線程B 中的任意操作happens-before 于線程A 的ThreadB.join()操作的成功返回.

2.4.3 程序順序規(guī)則

  • 兩個(gè)具有happens-before 關(guān)系的操作, 僅僅要求前一個(gè)操作(的執(zhí)行結(jié)果)對(duì)后一個(gè)操作可見.
    • 而不要求前一個(gè)操作要在后一個(gè)操作之前執(zhí)行.
    • 如果A happens-before B, 但A 和B 之間不存在數(shù)據(jù)依賴性, 則可能會(huì)進(jìn)行重排序, 使得B 在A 之前執(zhí)行.

2.5 重排序

2.5.1 數(shù)據(jù)依賴性

  • 如果兩個(gè)操作訪問同一變量, 且有一個(gè)是寫操作, 則這兩個(gè)操作存在數(shù)據(jù)依賴性.
  • 它僅針對(duì)單個(gè)CPU 中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作.

2.5.2 as-if-serial 語義

  • 無論如何重排序, 單線程程序的執(zhí)行結(jié)果不能被改變.
  • 為了遵守該語義, 編譯器和CPU 不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序.
  • 造成了一個(gè)幻覺: 單線程程序是按程序的順序來執(zhí)行的.

2.5.3 從源碼到指令序列的重排序

  • 從JAVA 源代碼到最終實(shí)際執(zhí)行的指令序列, 會(huì)經(jīng)歷3種重排序:
    • 編譯器優(yōu)化重排序: 在不改變單線程程序語義的前提下, 重新安排語句的執(zhí)行順序.
    • 指令級(jí)并行的重排序: 如果不存在數(shù)據(jù)依賴性, CPU 可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序.
      • 采用ILP(指令級(jí)并行技術(shù)) 來將多條指令重疊執(zhí)行.
    • 內(nèi)存系統(tǒng)的重排序: 由于CPU 私用緩存和讀/寫緩沖區(qū), 加載和存儲(chǔ)操作看起來是在亂序執(zhí)行.
    • 1 屬于編譯器重排序, 2和3 屬于處理器重排序.

2.5.4 JMM 的設(shè)計(jì)初衷

  • 程序員希望內(nèi)存模型易于理解和編程, 所以需要一個(gè)強(qiáng)內(nèi)存模型.
  • 編譯器和CPU 則希望內(nèi)存模型對(duì)其有最小的束縛, 方便做優(yōu)化來提高性能. 所以需要一個(gè)弱內(nèi)存模型.
  • 重排序會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題.
    • JMM 的編譯器重排序規(guī)則會(huì)禁止特定類型的重排序.
    • JMM 的處理器重排序規(guī)則會(huì)在生成指令序列時(shí), 插入特定類型的內(nèi)存屏障來禁止特定類型的重排序.
  • JMM 屬于語言級(jí)的內(nèi)存模型. 它確保在不同的編譯器和處理器平臺(tái)上, 通過禁止特定類型的編譯器和處理器重排序, 對(duì)外提供一致的內(nèi)存可見性保證.

2.5.5 JMM 對(duì)待重排序的策略

  • 對(duì)于會(huì)改變程序執(zhí)行結(jié)果的重排序, JMM 要求編譯器和CPU 必須禁止這種重排序.
  • 對(duì)于不會(huì)改變程序執(zhí)行結(jié)果的重排序, JMM 不做任何要求, 即允許這種重排序.
    • 例如, 若認(rèn)定鎖只會(huì)被單線程訪問, 則消除之; 若volatile 只會(huì)被單線程訪問, 則已普通變量對(duì)待之.

2.5.6 控制依賴關(guān)系

  • 前序操作是條件語句(if, while...), 則后續(xù)操作和前序之間就產(chǎn)生了控制依賴關(guān)系.
  • 當(dāng)代碼中存在控制依賴性時(shí), 會(huì)影響指令序列執(zhí)行的并行度.
    • 采用猜測(cè)(Speculation) 執(zhí)行來克服控制相關(guān)性對(duì)并行度的影響.
    • 例如, CPU 會(huì)執(zhí)行后續(xù)操作,并將其計(jì)算結(jié)果保存到重排序緩存(Reorder Buffer: ROB) 的硬件緩存中,如果條件為真, 直接使用該結(jié)果順序執(zhí)行.
    • 在多線程中, 對(duì)存在控制依賴的操作重排序, 可能會(huì)改變程序的執(zhí)行結(jié)果.


2.6 順序一致性

2.6.1 數(shù)據(jù)競(jìng)爭(zhēng)與順序一致性

  • 數(shù)據(jù)競(jìng)爭(zhēng):
    • 兩個(gè)線程對(duì)同一個(gè)變量, 分別進(jìn)行讀和寫, 且沒有通過同步對(duì)讀寫進(jìn)行排序.
    • 包含數(shù)據(jù)競(jìng)爭(zhēng)的代碼, 執(zhí)行結(jié)果不定.
  • 順序一致性:
    • 正確同步的程序, 其執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果.
    • 同步指的是對(duì)同步原語(synchronized, volatile和final)的使用.

2.6.2 順序一致性內(nèi)存模型

未同步程序在JMM 中的執(zhí)行, 整體是無序的, 其執(zhí)行結(jié)果無法預(yù)知. 而在順序一致性模型中, 所有線程看到的是一個(gè)一致的整體執(zhí)行順序.

  • 對(duì)外提供了極強(qiáng)的內(nèi)存可見性保證.
    • 一個(gè)線程中的所有操作必須按照程序的順序執(zhí)行.
    • 不管程序是否同步, 所有線程都只能看到一個(gè)單一的操作執(zhí)行順序. 每個(gè)操作都必須原子執(zhí)行并立刻對(duì)所有線程可見.
  • 一個(gè)單一的全局內(nèi)存, 通過左右搖擺的開關(guān)連接到任意一個(gè)(僅一個(gè))線程.
  • 例如, 兩個(gè)線程A 和B, 分別有操作A1, A2, A3, B1, B2, B3.
    • 在使用監(jiān)視器鎖來正確同步(A 先B 后)時(shí), 執(zhí)行順序?yàn)? A1 -> A2 -> A3 -> B1 -> B2 -> B3.
    • 在未同步時(shí), 可能的執(zhí)行順序是: A1 -> B1 -> B2 -> A2 -> A3 ->B3.
  • 未同步的多線程程序, 在順序一致性模型中雖然整體執(zhí)行順序是無序的, 但所有線程都只能看到一個(gè)一致的整體執(zhí)行順序(因?yàn)槊總€(gè)操作立即對(duì)任意線程可見).
    • 如果A 看到的是: A1 -> B1 -> B2 -> A2 -> A3 ->B3, 那么B 看到的也一定是.
  • 而未同步程序在JMM 中, 不但整體的執(zhí)行順序是無序的, 且所有線程看到的操作執(zhí)行順序也可能不一致(本地內(nèi)存不會(huì)及時(shí)的刷新到主內(nèi)存中).

2.6.3 未同步程序的執(zhí)行特性

  • 對(duì)于未同步或未正確同步的多線程程序, JMM 只提供最小安全性:
    • 線程執(zhí)行時(shí)讀取到的值, 要么是之前某個(gè)線程寫入的值, 要么是默認(rèn)值(0, false, null).
    • JMM 保證線程讀操作讀取到的值不會(huì)無中生有(Out of Thin Air).
  • JVM 在堆上分配對(duì)象時(shí), 首先會(huì)對(duì)內(nèi)存空間進(jìn)行清零, 然后在其上分配對(duì)象(內(nèi)部會(huì)同步這兩個(gè)操作).
    • 在已清零的內(nèi)存空間(Pre-zeroed Memory)分配對(duì)象時(shí), 域的默認(rèn)初始化已經(jīng)完成了.
  • JMM 不保證對(duì)64 位的long/double 型變量的寫操作具有原子性.
    • CPU 和內(nèi)存間的數(shù)據(jù)傳遞, 通過總線事務(wù)來確保所有CPU 對(duì)內(nèi)存的訪問以串行化的方式進(jìn)行.
    • 任意時(shí)刻, 最多只能有一個(gè)CPU 可以訪問內(nèi)存, 確保了單個(gè)總線事務(wù)之中的內(nèi)存讀寫操作具有原子性.
    • 在一些32 位CPU 上, 保證64 位數(shù)據(jù)寫操作的原子性, 會(huì)產(chǎn)生較大的開銷.
    • JMM 可能會(huì)將一個(gè)64 位寫操作分拆為兩個(gè)32 位的寫, 從而被分配到不同的總線事務(wù)上, 不再具有原子性.
    • JDK 1.5 以后, 保證了讀的原子性, 而寫允許被分拆.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,416評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,170評(píng)論 0 287
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,597評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評(píng)論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,029評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評(píng)論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,403評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,746評(píng)論 2 370

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

  • 并發(fā)系列的文章都是根據(jù)閱讀《Java 并發(fā)編程的藝術(shù)》這本書總結(jié)而來,想更深入學(xué)習(xí)的同學(xué)可以自行購買此書進(jìn)行學(xué)習(xí)。...
    小之丶閱讀 1,062評(píng)論 1 7
  • 原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽光一路滄桑 詳情請(qǐng)戳www.codercc.com # 1. JMM的介紹 ...
    你聽___閱讀 39,582評(píng)論 12 116
  • 相關(guān)文章Java并發(fā)編程(一)線程定義、狀態(tài)和屬性 Java并發(fā)編程(二)同步Java并發(fā)編程(三)volatil...
    劉望舒閱讀 1,366評(píng)論 0 21
  • 上個(gè)星期做了一件事——照顧小孩,都是六七歲的小孩子,個(gè)個(gè)都是天真無邪的模樣。 上午的時(shí)間只有一個(gè)小女孩,看著挺文靜...
    T無厘頭閱讀 175評(píng)論 0 0
  • 01 一個(gè)微風(fēng)涼涼的初夏夜晚,簡(jiǎn)瑤回到寢室,打開電腦,這是一天之中最放松的時(shí)候。忙忙碌碌地上完一天的課程,現(xiàn)在終于...
    舒子木閱讀 791評(píng)論 3 4