Java 內存模型

線程通訊與同步
程序員

在併發編程中,有兩個須要處理的關鍵問題:編程

  • 線程之間如何通訊
  • 線程之間如何同步

通訊指線程之間以何種機制來交換信息,通訊機制有兩種:緩存

  • 共享內存:經過讀 - 寫內存中的公共狀態進行隱式通訊
  • 消息傳遞:線程之間沒有公共狀態,線程之間必須經過發送消息來顯式進行通訊

同步是指程序中用於控制不一樣線程間操做發生的相對順序的機制。在共享內存併發模型中,同步是顯式進行的,程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式的安全


Java 內存模型

前面提到線程的通訊與同步問題,Java 線程之間的通訊由 Java 內存模型(簡稱 JMM)控制,JMM 決定一個線程對共享變量的寫入什麼時候對另外一個線程可見多線程

線程之間的共享變量存儲在主內存,每一個線程都一個私有的本地內存,本地內存中存儲了該線程以 讀/寫 共享變量的副本併發

Java 內存模型的抽象示意如圖app

若是線程 A 和線程 B 之間要通訊的話,必須通過下面兩個步驟:ide

  1. 線程 A 把本地內存 A 中更新過的共享變量刷新到主內存中
  2. 線程 B 到主內存中去讀取線程 A 以前已更新的共享變量

這兩個步驟其實是線程 A 向線程 B 發送消息,並且這個通訊過程必須通過主內存。JMM 經過控制主內存與每一個線程的本地內存之間的交互,來爲 Java 程序員提供內存可見性保證性能


重排序

在執行程序時,爲了提升性能,編譯器和處理器經常會對指令作重排序。重排序可能會致使線程程序出現內存可見性問題,下面分別介紹三種類型的重排序以及它們對內存可見性的影響:優化

  1. 編譯器優化的重排序

    編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序

  2. 指令級並行的重排序

    現代處理器採用了指令級並行技術來將多條指令重疊執行,若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序

  3. 內存系統的重排序

    因爲處理器使用緩存和 讀/寫 緩衝區,這使得加載和存儲操做看上去多是在亂序執行

    這裏對第三種狀況作個詳細解釋,現代處理器使用緩衝區臨時保存向內存寫入的數據,此舉能夠保證指令流水線持續進行,避免因爲處理器停頓下來等待向內存寫入數據而產生延遲。但每一個處理器上的寫緩衝區,僅僅對它所在的處理器可見,這個特性可能會致使處理器對內存的 讀/寫 操做執行順序不必定與內存實際發生的 讀/寫 操做順序一致。爲了說明狀況,請看下錶:


    Processor A Processor B
    代碼 a = 1;  // A1
    x = b;  // A2
    b = 2;  // B1
    y = a;  // B2

    處理器 A 和處理器 B 按程序的順序並行執行內存訪問,最終可能獲得 x = y = 0 的結果,具體緣由如圖

    處理器 A 和 處理器 B 同時把共享變量寫入本身的緩衝區(A一、B1),而後從內存中讀取另外一個共享變量(A二、B2),最後才把緩存區中保存的髒數據刷新到內存中(A三、B3),這種狀況下,程序最後就獲得 x = y = 0 的結果

因此 Java 源代碼到最終實際執行的指令序列,會分別經歷如下三種重排序


JMM 保證內存可見性

因而可知,JMM 不能任由重排序發生,必須加以控制,不然會引起線程不安全問題。爲了更好地解釋 JMM 爲保證內存可見性所採起的措施,首先介紹一些基礎概念

1. 數據依賴性

若是兩個操做訪問同一個變量,且這兩個操做有一個爲寫操做,此時這兩個操做之間就存在數據依賴性,只要重排序這兩個操做的執行順序,程序的執行結果就會被改變。編譯器和處理器在重排序時,會遵照數據依賴性,不會改變存在數據依賴關係的兩個操做的執行順序。數據依賴分爲下列三種類型:

名稱 代碼示例 說明
寫後讀 a = 1;
b = a;
寫一個變量以後,再讀這個位置
寫後寫 a = 1;
a = 2;
寫一個變量以後,再寫這個變量
讀後寫 a = b;
b = 1;
讀一個變量以後,再寫這個變量

上面三種狀況,只要重排序兩個操做的執行順序,程序的執行結果就會被改變

這裏所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操做,不一樣處理器之間和不一樣線程之間的數據依賴性不被編譯器和處理器考慮

2. as-if-serial 語義

as-if-serial 語義的意思是:無論怎麼重排序,(單線程)程序的執行結果不能被改變。編譯器、runtime 和處理器都必須遵照 as-if-serial 語義,爲了遵照 as-if-serial 語義,編譯器和處理器不會對存在數據依賴性關係的操做作重排序

as-if-serial 語義把單線程程序保護起來,給程序員建立了一個幻覺:單線程程序是按程序的順序來執行的,程序員無需擔憂重排序會干擾他們,也無需擔憂內存可見性問題

3. happens-before 原則

happens-before 是 JMM 最核心的概念,對於 Java 程序員來講,理解 happens-before 是理解 JMM 的關鍵。

從 JDK5 開始,Java 使用新的 JSR-133 內存模型,JSR-133 使用 happens-before 的概念來闡述操做之間的內存可見性。在 JMM 中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在 happens-before 關係,這兩個操做既能夠是在一個線程以內,也能夠是不一樣線程之間

A happens-before B,就是 A 操做先於 B 操做執行。固然這種說法並不許確,兩個操做之間具備 happens-before 關係,僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前

在 JMM 中定義了 happens-before 的原則以下:

  • 單線程 happens-before 原則:在同一個線程中,書寫在前面的操做 happens-before 後面的操做
  • 鎖的 happens-before 原則:同一個鎖的 unlock 操做 happens-before 此鎖的 lock 操做
  • volatile 的 happens-before 原則:對一個 volatile 變量的寫操做 happens-before 對此變量的任意操做(固然也包括寫操做)
  • happens-before 的傳遞性原則:若是 A 操做 happens-before B 操做,B 操做 happens-before C 操做,那麼 A 操做 happens-before C 操做
  • 線程啓動的 happens-before 原則:同一個線程的 start 方法 happens-before 此線程的其它方法
  • 線程中斷的 happens-before 原則:對線程 interrupt 方法的調用 happens-before 被中斷線程的檢測到中斷髮送的代碼
  • 線程終結的 happens-before 原則:線程中的全部操做都 happens-before 線程的終止檢測
  • 對象建立的 happens-before 原則:一個對象的初始化完成先於他的 finalize 方法調用

有關 happens-before 每個原則的實現,這裏再也不具體闡述,只要知道有這麼一回事就行了

4. 順序一致性內存模型

順序一致性內存模型是一個被計算機科學家理想化了的理論參考模型,它爲程序員提供了極強的內存可見性保證。順序一致性內存模型有兩大特徵:

  • 一個線程中的全部操做必須按照程序的順序來執行
  • 無論程序是否同步,全部線程都只能看到一個單一的操做執行順序。在順序一致性內存模型中,每一個操做都必須原子執行且馬上對全部線程可見

順序一致性內存模型爲程序員提供的視圖以下:

在概念上,順序一致性模型有一個單一的全局內存,這個內存經過一個左右擺動的開關能夠鏈接到任意一個線程。同時,每個線程必須按程序的順序來執行內存讀/寫操做。從上圖咱們能夠看出,在任意時間點最多隻能有一個線程能夠鏈接到內存。當多個線程併發執行時,圖中的開關裝置能把全部線程的全部內存 讀/寫 操做串行化

爲了更好的理解,下面咱們經過兩個示意圖來對順序一致性模型的特性作進一步的說明

假設有兩個線程 A 和 B 併發執行,其中 A 線程有三個操做,它們在程序中的順序是:A1 -> A2 -> A3,B 線程也有三個操做,它們在程序中的順序是:B1 -> B2 -> B3

假設這兩個線程使用監視器來正確同步:A 線程的三個操做執行後釋放監視器,隨後 B 線程獲取同一個監視器。那麼程序在順序一致性模型中的執行效果將以下圖所示:

如今再假設這兩個線程沒有作同步,下面是這個未同步程序在順序一致性模型中的執行示意圖:

未同步程序在順序一致性模型中雖然總體執行順序是無序的,但全部線程都只能看到一個一致的總體執行順序。以上圖爲例,線程 A 和 B 看到的執行順序都是:B1->A1->A2->B2->A3->B3。之因此能獲得這個保證是由於順序一致性內存模型中的每一個操做必須當即對任意線程可見

5. 總結

因爲重排序的存在,JMM 不可能實現順序一致性內存模型,同時也不可能徹底禁止重排序,由於這樣會影響效率。一方面,程序員但願內存模型易於理解、易於編程,但願基於一個強內存模型來編寫代碼;另外一方面,編譯器和處理器但願內存模型對它們的束縛越少越好,這樣它們就能夠作儘量多的優化來提升性能,編譯器和處理器但願實現一個弱內存模型。這兩個因素相互矛盾,因此關鍵在於找到一個平衡點

平衡的關鍵在於優化重排序規則,根據前面提到的 happens-before 原則、數據依賴性以及 as-if-serial 原則等規定了編譯器和處理器什麼狀況容許重排序,什麼狀況不容許重排序。對於會改變程序執行結果的重排序,JMM 要求編譯器和處理器必須禁止這種重排序,不然不做要求。因而程序員所看到的就是一個保證了內存可見性的可靠的內存模型

下圖是 JMM 的設計示意圖

從上圖咱們也能夠發現,JMM 會遵循一個基本原則:只要不改變程序的執行結果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎麼優化均可以。例如,若是編譯器通過細緻地分析後,認定一個鎖只會被單個線程訪問,那麼這個鎖能夠被消除。再如,若是編譯器通過細緻的分析後,認定一個 volatile 變量只會被單個線程訪問,那麼編譯器能夠把這個 volatile 變量看成一個普通變量來對待。這些優化既不會改變程序的執行結果,又能提升程序的執行效率。而從程序員的角度來看,程序員其實並不關心重排序是否真的發生,程序員關心的是隻程序執行時的語義不能被改變而已

相關文章
相關標籤/搜索