Java 并發編程(1): Java 內存模型(JMM)

1. 并發編程

1.1 并發編程的挑戰

并發編程的目的是為了加快程序的運行速度, 但受限于上下文切換和死鎖等問題, 啟動更多的線程并非能讓程序最大限度地并行執行.

1.1.1 上下文切換

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

1.1.2 死鎖

  • 一旦產生死鎖, 就會造成系統功能不可用.
    • 此時, 業務是可感知的, 因為不能繼續提供服務了.
    • 通過dump 線程來查看到底是那個線程出現了問題.
  • 避免死鎖的常用手段:
    • 避免一個線程同時獲取多個鎖.
    • 避免一個線程在持有鎖期間同時占用多個資源, 盡量保證每個鎖只占用一個資源.
    • 嘗試使用定時鎖. 例如 lock.tryLock(timeout).

1.2 資源限制的挑戰

只有在串行執行會浪費資源時, 將其修改為并行執行才能加快運行速度.

  • 在并發編程時, 程序的執行速度受限于PC 硬件資源或軟件資源, 如網速, 硬盤讀寫速度, CPU 處理速度, 數據庫的鏈接數和socket 連接數等.
  • 資源限制引發的問題:
    • 在資源受限時, 將串行執行的代碼并發執行, 其結果仍然是串行執行. 由于增加了上下文切換和資源調度的消耗, 可能會使得程序的運行速度更慢.
    • 例如, 在較慢的網絡條件下, 下載一個大文件, 單線程比并發編程速度更快.

2. Java 內存模型(JMM)

現代軟硬件的共同目標: 在不改變程序執行結果的前提下, 盡可能提高并行度.

2.1 內存模型的基礎

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

2.2 JMM 的抽象結構

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


  • JMM 決定一個線程對共享變量的寫入何時對另一個線程可見.
    • 定義了線程的本地內存和主內存之間的抽象關系.
    • 主內存負責存儲共享變量.
    • 本地內存涵蓋了緩存,寫緩存,寄存器及其他優化. 它會存儲該線程讀寫共享變量的副本.
    • 兩個線程間的通信過程, 必須經過主內存.

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

  • CPU 會使用寫緩存區來臨時保存需要向內存寫入的數據.
    • 避免由于處理器停頓下來等待向內存寫入數據而產生的延遲, 從而保證了指令流水線持續運行.
    • 同時, 通過以批處理的方式刷新寫緩存, 以及合并對同一內存地址的多次寫, 減少了對內存總線的占用.
  • 但是, 每個CPU 上的寫緩存區, 僅對該CPU 可見.
    • 該特性會對內存操作的執行順序產生影響: CPU 對內存的讀寫操作的執行順序, 不一定與內存實際發生的讀寫順序一致.
    • 由此, CPU 允許對寫-讀操作進行重排序(因為本來也無法保證其順序性, 且重排序能夠提升性能).
  • 在適當的位置插入內存屏障來禁止特定類型的CPU 重排序.
屏障類型 指令示例
LoadLoad Barries Load1; LoadLoad; Load2
StoreStore Barries Store1; StoreStore; Store2
Load?Store Barries Load1; LoadStore; Store2
StoreLoad Barries Store1; StoreLoad; Load2
  • 其中, StoreLoad 同時具有其它3個屏障的效果, 執行它的開銷很大, 因為它需要把寫緩存區的數據全部刷新到內存中(Buffer Fully Flush).

2.4 happens-before

2.4.1 happens-before 的定義

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

2.4.2 happens-before 規則:

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

2.4.3 程序順序規則

  • 兩個具有happens-before 關系的操作, 僅僅要求前一個操作(的執行結果)對后一個操作可見.
    • 而不要求前一個操作要在后一個操作之前執行.
    • 如果A happens-before B, 但A 和B 之間不存在數據依賴性, 則可能會進行重排序, 使得B 在A 之前執行.

2.5 重排序

2.5.1 數據依賴性

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

2.5.2 as-if-serial 語義

  • 無論如何重排序, 單線程程序的執行結果不能被改變.
  • 為了遵守該語義, 編譯器和CPU 不會對存在數據依賴關系的操作做重排序.
  • 造成了一個幻覺: 單線程程序是按程序的順序來執行的.

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

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

2.5.4 JMM 的設計初衷

  • 程序員希望內存模型易于理解和編程, 所以需要一個強內存模型.
  • 編譯器和CPU 則希望內存模型對其有最小的束縛, 方便做優化來提高性能. 所以需要一個弱內存模型.
  • 重排序會導致多線程程序出現內存可見性問題.
    • JMM 的編譯器重排序規則會禁止特定類型的重排序.
    • JMM 的處理器重排序規則會在生成指令序列時, 插入特定類型的內存屏障來禁止特定類型的重排序.
  • JMM 屬于語言級的內存模型. 它確保在不同的編譯器和處理器平臺上, 通過禁止特定類型的編譯器和處理器重排序, 對外提供一致的內存可見性保證.

2.5.5 JMM 對待重排序的策略

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

2.5.6 控制依賴關系

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


2.6 順序一致性

2.6.1 數據競爭與順序一致性

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

2.6.2 順序一致性內存模型

未同步程序在JMM 中的執行, 整體是無序的, 其執行結果無法預知. 而在順序一致性模型中, 所有線程看到的是一個一致的整體執行順序.

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

2.6.3 未同步程序的執行特性

  • 對于未同步或未正確同步的多線程程序, JMM 只提供最小安全性:
    • 線程執行時讀取到的值, 要么是之前某個線程寫入的值, 要么是默認值(0, false, null).
    • JMM 保證線程讀操作讀取到的值不會無中生有(Out of Thin Air).
  • JVM 在堆上分配對象時, 首先會對內存空間進行清零, 然后在其上分配對象(內部會同步這兩個操作).
    • 在已清零的內存空間(Pre-zeroed Memory)分配對象時, 域的默認初始化已經完成了.
  • JMM 不保證對64 位的long/double 型變量的寫操作具有原子性.
    • CPU 和內存間的數據傳遞, 通過總線事務來確保所有CPU 對內存的訪問以串行化的方式進行.
    • 任意時刻, 最多只能有一個CPU 可以訪問內存, 確保了單個總線事務之中的內存讀寫操作具有原子性.
    • 在一些32 位CPU 上, 保證64 位數據寫操作的原子性, 會產生較大的開銷.
    • JMM 可能會將一個64 位寫操作分拆為兩個32 位的寫, 從而被分配到不同的總線事務上, 不再具有原子性.
    • JDK 1.5 以后, 保證了讀的原子性, 而寫允許被分拆.
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容