Java 內存模型

語言: CN / TW / HK

theme: vuepress highlight: atom-one-dark


小知識,大挑戰!本文正在參與“程序員必備小知識”創作活動

         理解Java內存模型是深入學習Java併發不可或缺的部分。Java內存模型即Java Memory Model,簡稱為JMM,定義了多線程之間共享變量的可見性以及如何在需要的時候對共享變量進行同步。

        JMM規定Java線程間的通信採用共享內存的方式。在Java中,所有成員變量、靜態變量和數組元素都存儲在堆內存中,堆內存在線程之間共享,所以它們通常也稱為共享變量。

        JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory,或者也可以稱為工作內存 Work Memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器以及其他的硬件和編譯器優化。

JMM抽象

JMM的抽象示意圖如下所示:

jmm01.png

多個線程同時對同一個共享變量進行讀寫的時候會產生線程安全問題。那為什麼CPU不直接操作內存,而要在CPU和內存間加上各種緩存和寄存器等緩衝區呢?因為CPU的運算速度要比內存的讀寫速度快得多,如果CPU直接操作內存的話勢必會花費很長時間等待數據到來,所以緩存的出現主要是為了解決CPU運算速度與內存讀寫速度不匹配的矛盾。

內存間交互協議

JMM規定了主內存和工作內存間具體的交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步到主內存之間的實現細節,這主要包含了下面8個步驟:

jmm02.png - lock(鎖定):作用於主內存的變量,把一個變量標識為一條線程獨佔狀態。 - unlock(解鎖):作用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定。 - read(讀取):作用於主內存變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用 - load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。 - use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時將會執行這個操作。 - assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。 - store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨後的write的操作。 - write(寫入):作用於主內存的變量,它把store操作從工作內存中一個變量的值傳送到主內存的變量中。 lock,unlock需要代碼中用鎖實現。

這8個步驟必須符合下述規則: 1. 不允許read和load,store和write操作之一單獨出現。 2. 不允許一個線程丟棄它最近的assign操作。即變量在工作內存中改變了賬號必須把變化同步回主內存。 3. 一個新的變量只允許在主內存中誕生,不允許工作內存直接使用未初始化的變量。 4. 一個變量同一時刻只允許一條線程進行lock操作,但同一線程可以lock多次,lock多次之後必須執行同樣次數的unlock操作。 5. 如果對一個變量進行lock操作,那麼將會清空工作內存中此變量的值。 6. 不允許對未lock的變量進行unlock操作,也不允許unlock一個被其它線程lock的變量。 7. 如果一個變量執行unlock操作,必須先把此變量同步回主內存中。

指令重排

在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。從Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序:

  1. 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-LevelParallelism,ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 內存系統的重排序。由於處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。

如果兩個操作訪問同一個變量,其中一個為寫操作,此時這兩個操作之間存在數據依賴性。 編譯器和處理器不會改變存在數據依賴性關係的兩個操作的執行順序,即不會重排序。不管怎麼重排序,單線程下的執行結果不能被改變,編譯器、runtime和處理器都必須遵守as-if-serial語義。

內存屏障

通過插入內存屏障(Memory Barrier)可以阻止特定類型的指令重排。JMM將內存屏障劃分為四種: | 屏障類型 | 示例 | 描述 | | :------------------ | :----------------------- | :----------------------------------------------------------- | | LoadLoad Barriers | Load1-LoadLoad-Load2 | Load1數據裝載過程要先於Load2及所有後續的數據裝載過程 | | StoreStore Barriers | Store1-StoreStore-Store2 | Store1刷新數據到內存的過程要先於Strore2及後續所有刷新數據到內存的過程 | | LoadStore Barriers | Load1-LoadStore-Store2 | Load1數據裝載要先於Strore2及後續所有刷新數據到內存的過程 | | StoreLoad Barriers | Store1-StoreLoad-Load2 | Store1刷新數據到內存的過程要先於Load2及所有後續的數據裝載過程 |

Java中volatile關鍵字的實現就是通過內存屏障來完成的。

happens-before

從jdk5開始,java使用新的JSR-133內存模型,基於happens-before的概念來闡述操作之間的內存可見性。

在JMM中,如果一個操作的執行結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係,這個的兩個操作既可以在同一個線程,也可以在不同的兩個線程中。

與程序員密切相關的happens-before規則如下:

  • 程序順序規則:一個線程中的每個操作,happens-before於該線程中任意的後續操作。
  • 監視器鎖規則:對一個鎖的解鎖操作,happens-before於隨後對這個鎖的加鎖操作。
  • volatile域規則:對一個volatile域的寫操作,happens-before於任意線程後續對這個volatile域的讀。
  • 傳遞性規則:如果 A happens-before B,且 B happens-before C,那麼A happens-before C。

注意:兩個操作之間具有happens-before關係,並不意味前一個操作必須要在後一個操作之前執行!僅僅要求前一個操作的執行結果,對於後一個操作是可見的,且前一個操作按順序排在後一個操作之前。