深入理解 Java 內存模型

2021-03-05    分類: 網(wǎng)站建設

前提

《深入理解 Java 內存模型》程曉明著,該書在以前看過一遍,現(xiàn)在學的東西越多,感覺那塊越重要,于是又再細看一遍,于是便有了下面的讀書筆記總結。全書頁數(shù)雖不多,內容講得挺深的。細看的話,也是挺花時間的,看完收獲絕對挺大的。也 基礎

并發(fā)編程的模型分類

在并發(fā)編程需要處理的兩個關鍵問題是:線程之間如何通信和線程之間如何同步。


通信

通信是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通信機制有兩種:共享內存和消息傳遞。

在共享內存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),線程之間通過寫-讀內存中的公共狀態(tài)來隱式進行通信。

在消息傳遞的并發(fā)模型里,線程之間沒有公共狀態(tài),線程之間必須通過明確的發(fā)送消息來顯式進行通信。


同步

同步是指程序用于控制不同線程之間操作發(fā)生相對順序的機制。

在共享內存的并發(fā)模型里,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執(zhí)行。

在消息傳遞的并發(fā)模型里,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式進行的。

Java 的并發(fā)采用的是共享內存模型,Java 線程之間的通信總是隱式進行,整個通信過程對程序員完全透明。


Java 內存模型的抽象

在 Java 中,所有實例域、靜態(tài)域 和 數(shù)組元素存儲在堆內存中,堆內存在線程之間共享。局部變量、方法定義參數(shù) 和 異常處理器參數(shù) 不會在線程之間共享,它們不會有內存可見性問題,也不受內存模型的影響。

Java 線程之間的通信由 Java 內存模型(JMM)控制。JMM 決定了一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM 定義了線程與主內存之間的抽象關系:線程之間的共享變量存儲在主內存中,每一個線程都有一個自己私有的本地內存,本地內存中存儲了該變量以讀/寫共享變量的副本。本地內存是 JMM 的一個抽象概念,并不真實存在。

JMM 抽象示意圖:

從上圖來看,如果線程 A 和線程 B 要通信的話,要如下兩個步驟:

1、線程 A 需要將本地內存 A 中的共享變量副本刷新到主內存去

2、線程 B 去主內存讀取線程 A 之前已更新過的共享變量

步驟示意圖:

舉個例子:

本地內存 A 和 B 有主內存共享變量 X 的副本。假設一開始時,這三個內存中 X 的值都是 0。線程 A 正執(zhí)行時,把更新后的 X 值(假設為 1)臨時存放在自己的本地內存 A 中。當線程 A 和 B 需要通信時,線程 A 首先會把自己本地內存 A 中修改后的 X 值刷新到主內存去,此時主內存中的 X 值變?yōu)榱?1。隨后,線程 B 到主內存中讀取線程 A 更新后的共享變量 X 的值,此時線程 B 的本地內存的 X 值也變成了 1。

整體來看,這兩個步驟實質上是線程 A 再向線程 B 發(fā)送消息,而這個通信過程必須經(jīng)過主內存。JMM 通過控制主內存與每個線程的本地內存之間的交互,來為 Java 程序員提供內存可見性保證。


重排序

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

1、編譯器優(yōu)化的重排序。編譯器在不改變指令級并行的重排序?,F(xiàn)代處理器采用了指令級并行技術來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應機器指令的執(zhí)行順序。

3、內存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。

從 Java 源代碼到最終實際執(zhí)行的指令序列,會分別經(jīng)歷下面三種重排序:

上面的這些重排序都可能導致多線程程序出現(xiàn)內存可見性問題。對于編譯器,JMM 的編譯器重排序規(guī)則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對于處理器重排序,JMM 的處理器重排序規(guī)則會要求 Java 編譯器在生成指令序列時,插入特定類型的內存屏障指令,通過內存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。

JMM 屬于語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內存可見性保證。


處理器重排序

現(xiàn)代的處理器使用寫緩沖區(qū)來臨時保存向內存寫入的數(shù)據(jù)。寫緩沖區(qū)可以保證指令流水線持續(xù)運行,它可以避免由于處理器停頓下來等待向內存寫入數(shù)據(jù)而產(chǎn)生的延遲。同時,通過以批處理的方式刷新寫緩沖區(qū),以及合并寫緩沖區(qū)中對同一內存地址的多次寫,可以減少對內存總線的占用。雖然寫緩沖區(qū)有這么多好處,但每個處理器上的寫緩沖區(qū),僅僅對它所在的處理器可見。這個特性會對內存操作的執(zhí)行順序產(chǎn)生重要的影響:處理器對內存的讀/寫操作的執(zhí)行順序,不一定與內存實際發(fā)生的讀/寫操作順序一致!

舉個例子:

假設處理器A和處理器B按程序的順序并行執(zhí)行內存訪問,最終卻可能得到 x = y = 0。具體的原因如下圖所示:

處理器 A 和 B 同時把共享變量寫入在寫緩沖區(qū)中(A1、B1),然后再從內存中讀取另一個共享變量(A2、B2),最后才把自己寫緩沖區(qū)中保存的臟數(shù)據(jù)刷新到內存中(A3、B3)。當以這種時序執(zhí)行時,程序就可以得到 x = y = 0 的結果。

從內存操作實際發(fā)生的順序來看,直到處理器 A 執(zhí)行 A3 來刷新自己的寫緩存區(qū),寫操作 A1 才算真正執(zhí)行了。雖然處理器 A 執(zhí)行內存操作的順序為:A1 -> A2,但內存操作實際發(fā)生的順序卻是:A2 -> A1。此時,處理器 A 的內存操作順序被重排序了。

這里的關鍵是,由于寫緩沖區(qū)僅對自己的處理器可見,它會導致處理器執(zhí)行內存操作的順序可能會與內存實際的操作執(zhí)行順序不一致。由于現(xiàn)代的處理器都會使用寫緩沖區(qū),因此現(xiàn)代的處理器都會允許對寫-讀操作重排序。


內存屏障指令

為了保證內存可見性,Java 編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM 把內存屏障指令分為下列四類:


h一個操作執(zhí)行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在 h
  • 程序順序規(guī)則:一個線程中的每個操作,h 數(shù)據(jù)依賴性

    如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數(shù)據(jù)依賴性。數(shù)據(jù)依賴分下列三種類型:

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

    上面三種情況,只要重排序兩個操作的執(zhí)行順序,程序的執(zhí)行結果將會被改變。

    前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時,會遵守數(shù)據(jù)依賴性,編譯器和處理器不會改變存在數(shù)據(jù)依賴關系的兩個操作的執(zhí)行順序。

    注意,這里所說的數(shù)據(jù)依賴性僅針對單個處理器中執(zhí)行的指令序列和單個線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。


    as-if-serial 語義

    as-if-serial 語義的意思指:不管怎么重排序(編譯器和處理器為了提高并行度),(程序的執(zhí)行結果不能被改變。編譯器,runtime 和處理器都必須遵守 as-if-serial 語義。

    為了遵守 as-if-serial 編譯器和處理器不會對存在數(shù)據(jù)依賴關系的操作做重排序,因為這種重排序會改變執(zhí)行結果。但是如果操作之間沒有數(shù)據(jù)依賴關系,這些操作就可能被編譯器和處理器重排序。

    舉個例子:

    1double pi = 3.14; //A2double r = 1.0; //B3double area = pi * r * r; //C

    上面三個操作的數(shù)據(jù)依賴關系如下圖所示:

    如上圖所示,A 和 C 之間存在數(shù)據(jù)依賴關系,同時 B 和 C 之間也存在數(shù)據(jù)依賴關系。因此在最終執(zhí)行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的結果將會被改變)。但 A 和 B 之間沒有數(shù)據(jù)依賴關系,編譯器和處理器可以重排序 A 和 B 之間的執(zhí)行順序。下圖是該程序的兩種執(zhí)行順序:

    在計算機中,軟件技術和硬件技術有一個共同的目標:在不改變程序執(zhí)行結果的前提下,盡可能的開發(fā)并行度。編譯器和處理器遵從這一目標,從 h 重排序對多線程的影響

    舉例:

    1class Demo {2 int a = 0;3 boolean flag = false;45 public void write {6 a = 1; //17 flag = true; //28 }910 public void read {11 if(flag) { //312 int i = a * a; //413 }14 }15}

    由于操作 1 和 2 沒有數(shù)據(jù)依賴關系,編譯器和處理器可以對這兩個操作重排序;操作 3 和操作 4 沒有數(shù)據(jù)依賴關系,編譯器和處理器也可以對這兩個操作重排序。

    1、當操作 1 和操作 2 重排序時,可能會產(chǎn)生什么效果?

    如上圖所示,操作 1 和操作 2 做了重排序。程序執(zhí)行時,線程 A 首先寫標記變量 flag,隨后線程 B 讀這個變量。由于條件判斷為真,線程 B 將讀取變量 a。此時,變量 a 還根本沒有被線程 A 寫入,在這里多線程程序的語義被重排序破壞了!

    2、當操作 3 和操作 4 重排序時會產(chǎn)生什么效果(借助這個重排序,可以順便說明控制依賴性)。

    在程序中,操作 3 和操作 4 存在控制依賴關系。當代碼中存在控制依賴性時,會影響指令序列執(zhí)行的并行度。為此,編譯器和處理器會采用猜測(Speculation)執(zhí)行來克服控制相關性對并行度的影響。以處理器的猜測執(zhí)行為例,執(zhí)行線程 B 的處理器可以提前讀取并計算 a * a,然后把計算結果臨時保存到一個名為重排序緩沖(reorder buffer ROB)的硬件緩存中。當接下來操作 3 的條件判斷為真時,就把該計算結果寫入變量 i 中。

    從圖中我們可以看出,猜測執(zhí)行實質上對操作3和4做了重排序。重排序在這里破壞了多線程程序的語義!

    在 順序一致性

    順序一致性內存模型

    順序一致性內存模型有兩大特性:

    • 一個線程中的所有操作必須按照程序的順序來執(zhí)行。

    • (不管程序是否同步)所有線程都只能看到一個單一的操作執(zhí)行順序。在順序一致性內存模型中,每個操作都必須原子執(zhí)行且立刻對所有線程可見。

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

    在概念上,順序一致性模型有一個單一的全局內存,這個內存通過一個左右擺動的開關可以連接到任意一個線程,同時每一個線程必須按照程序的順序來執(zhí)行內存讀/寫操作。從上面的示意圖我們可以看出,在任意時間點最多只能有一個線程可以連接到內存。當多個線程并發(fā)執(zhí)行時,圖中的開關裝置能把所有線程的所有內存讀/寫操作串行化。

    舉個例子:

    假設有兩個線程 A 和 B 并發(fā)執(zhí)行。其中 A 線程有三個操作,它們在程序中的順序是:A1 -> A2 -> A3。B 線程也有三個操作,它們在程序中的順序是:B1 -> B2 -> B3。

    假設這兩個線程使用監(jiān)視器鎖來正確同步:A 線程的三個操作執(zhí)行后釋放監(jiān)視器鎖,隨后 B 線程獲取同一個監(jiān)視器鎖。那么程序在順序一致性模型中的執(zhí)行效果將如下圖所示:

    現(xiàn)在我們再假設這兩個線程沒有做同步,下面是這個未同步程序在順序一致性模型中的執(zhí)行示意圖:

    未同步程序在順序一致性模型中雖然整體執(zhí)行順序是無序的,但所有線程都只能看到一個一致的整體執(zhí)行順序。以上圖為例,線程 A 和 B 看到的執(zhí)行順序都是:B1 -> A1 -> A2 -> B2 -> A3 -> B3。之所以能得到這個保證是因為順序一致性內存模型中的每個操作必須立即對任意線程可見。

    但是,在 JMM 中就沒有這個保證。未同步程序在 JMM 中不但整體的執(zhí)行順序是無序的,而且所有線程看到的操作執(zhí)行順序也可能不一致。比如,在當前線程把寫過的數(shù)據(jù)緩存在本地內存中,在還沒有刷新到主內存之前,這個寫操作僅對當前線程可見;從其他線程的角度來觀察,會認為這個寫操作根本還沒有被當前線程執(zhí)行。只有當前線程把本地內存中寫過的數(shù)據(jù)刷新到主內存之后,這個寫操作才能對其他線程可見。在這種情況下,當前線程和其它線程看到的操作執(zhí)行順序將不一致。


    同步程序的順序一致性效果

    下面我們對前面的示例程序用鎖來同步,看看正確同步的程序如何具有順序一致性。

    請看下面的示例代碼:

    1class demo {2 int a = 0;3 boolean flag = false;45 public synchronized void write { //獲取鎖6 a = 1;7 flag = true;8 } //釋放鎖910 public synchronized void read { //獲取鎖11 if(flag) {12 int i = a;13 }14 } //釋放鎖15}

    上面示例代碼中,假設 A 線程執(zhí)行 write 方法后,B 線程執(zhí)行 reade 方法。這是一個正確同步的多線程程序。根據(jù)JMM規(guī)范,該程序的執(zhí)行結果將與該程序在順序一致性模型中的執(zhí)行結果相同。下面是該程序在兩個內存模型中的執(zhí)行時序對比圖:

    在順序一致性模型中,所有操作完全按程序的順序執(zhí)行。而在 JMM 中,臨界區(qū)內的代碼可以重排序(但 JMM 不允許臨界區(qū)內的代碼“逸出”到臨界區(qū)之外,那樣會破壞監(jiān)視器的語義)。JMM 會在退出臨界區(qū)和進入臨界區(qū)這兩個關鍵時間點做一些特別處理,使得線程在這兩個時間點具有與順序一致性模型相同的內存視圖。雖然線程 A 在臨界區(qū)內做了重排序,但由于監(jiān)視器的互斥執(zhí)行的特性,這里的線程 B 根本無法“觀察”到線程 A 在臨界區(qū)內的重排序。這種重排序既提高了執(zhí)行效率,又沒有改變程序的執(zhí)行結果。

    從這里我們可以看到 JMM 在具體實現(xiàn)上的基本方針:在不改變(正確同步的)程序執(zhí)行結果的前提下,盡可能的為編譯器和處理器的優(yōu)化打開方便之門。


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

    未同步程序在 JMM 中的執(zhí)行時,整體上是無序的,其執(zhí)行結果無法預知。未同步程序在兩個模型中的執(zhí)行特性有下面幾個差異:

    1. 順序一致性模型保證JMM 不保證對 64 位的 long 型和 double 型變量的讀/寫操作具有原子性,而順序一致性模型保證對所有的內存讀/寫操作都具有原子。

    第三個差異與處理器總線的工作機制密切相關。在計算機中,數(shù)據(jù)通過總線在處理器和內存之間傳遞。每次處理器和內存之間的數(shù)據(jù)傳遞都是通過總線事務來完成的??偩€事務包括讀事務和寫事務。讀事務從內存?zhèn)魉蛿?shù)據(jù)到處理器,寫事務從處理器傳遞數(shù)據(jù)到內存,每個事務會讀/寫內存中一個或多個物理上連續(xù)的字??偩€會同步試圖并發(fā)使用總線的事務。在一個處理器執(zhí)行總線事務期間,總線會禁止其它所有的處理器和 I/O 設備執(zhí)行內存的讀/寫。

    總線的工作機制:

    如上圖所示,假設處理器 A、B、和 C 同時向總線發(fā)起總線事務,這時總線仲裁會對競爭作出裁決,假設總線在仲裁后判定處理器 A 在競爭中獲勝(總線仲裁會確保所有處理器都能公平的訪問內存)。此時處理器 A 繼續(xù)它的總線事務,而其它兩個處理器則要等待處理器 A 的總線事務完成后才能開始再次執(zhí)行內存訪問。假設在處理器 A 執(zhí)行總線事務期間(不管這個總線事務是讀事務還是寫事務),處理器 D 向總線發(fā)起了總線事務,此時處理器 D 的這個請求會被總線禁止。

    總線的這些工作機制可以把所有處理器對內存的訪問以串行化的方式來執(zhí)行;在任意時間點,最多只能有一個處理器能訪問內存。這個特性確保了單個總線事務之中的內存讀/寫操作具有原子性。

    在一些 32 位的處理器上,如果要求對 64 位數(shù)據(jù)的寫操作具有原子性,會有比較大的開銷。為了照顧這種處理器,Java 語言規(guī)范鼓勵但不強求 JVM 對 64 位的 long 型變量和 double 型變量的寫具有原子性。當 JVM 在這種處理器上運行時,會把一個 64 位 long/ double 型變量的寫操作拆分為兩個 32 位的寫操作來執(zhí)行。這兩個 32 位的寫操作可能會被分配到不同的總線事務中執(zhí)行,此時對這個 64 位變量的寫將不具有原子性。

    當單個內存操作不具有原子性,將可能會產(chǎn)生意想不到后果。請看下面示意圖:

    如上圖所示,假設處理器 A 寫一個 long 型變量,同時處理器 B 要讀這個 long 型變量。處理器 A 中 64 位的寫操作被拆分為兩個 32 位的寫操作,且這兩個 32 位的寫操作被分配到不同的寫事務中執(zhí)行。同時處理器 B 中 64 位的讀操作被分配到單個的讀事務中執(zhí)行。當處理器 A 和 B 按上圖的時序來執(zhí)行時,處理器 B 將看到僅僅被處理器 A “寫了一半“的無效值。

    注意,在 JSR -133 之前的舊內存模型中,一個 64 位 long/ double 型變量的讀/寫操作可以被拆分為兩個 32 位的讀/寫操作來執(zhí)行。從 JSR -133 內存模型開始(即從JDK5開始),僅僅只允許把一個 64 位 long/ double 型變量的寫操作拆分為兩個 32 位的寫操作來執(zhí)行,任意的讀操作在JSR -133中都必須具有原子性(即任意讀操作必須要在單個讀事務中執(zhí)行)。


    Volatile

    Volatile 特性

    舉個例子:

    1public class VolatileTest {2 volatile long a = 1L; // 使用 volatile 聲明 64 位的 long 型34 public void set(long l) {5 a = l; //單個 volatile 變量的寫6 }78 public long get {9 return a; //單個 volatile 變量的讀10 }1112 public void getAndIncreament {13 a++; // 復合(多個) volatile 變量的讀 /寫14 }15}

    假設有多個線程分別調用上面程序的三個方法,這個程序在語義上和下面程序等價:

    1public class VolatileTest {2 long a = 1L; // 64 位的 long 型普通變量34 public synchronized void set(long l) { //對單個普通變量的寫用同一個鎖同步5 a = l;6 }78 public synchronized long get { //對單個普通變量的讀用同一個鎖同步9 return a;10 }1112 public void getAndIncreament { //普通方法調用13 long temp = get; //調用已同步的讀方法14 temp += 1L; //普通寫操作15 set(temp); //調用已同步的寫方法16 }17}

    如上面示例程序所示,對一個 volatile 變量的單個讀/寫操作,與對一個普通變量的讀/寫操作使用同一個鎖來同步,它們之間的執(zhí)行效果相同。

    鎖的 h對一個 volatile 變量的讀,總是能看到(任意線程)對這個 volatile 變量最后的寫入。

    鎖的語義決定了臨界區(qū)代碼的執(zhí)行具有原子性。這意味著即使是 64 位的 long 型和 double 型變量,只要它是 volatile變量,對該變量的讀寫就將具有原子性。如果是多個 volatile 操作或類似于 volatile++ 這種復合操作,這些操作整體上不具有原子性。

    簡而言之,volatile 變量自身具有下列特性:

    • 可見性。對一個 volatile 變量的讀,總是能看到(任意線程)對這個 volatile 變量最后的寫入。

    • 原子性:對任意單個 volatile 變量的讀/寫具有原子性,但類似于 volatile++ 這種復合操作不具有原子性。


    volatile 寫-讀的內存定義

    • 當寫一個 volatile 變量時,JMM 會把該線程對應的本地內存中的共享變量值刷新到主內存。

    • 當讀一個 volatile 變量時,JMM 會把該線程對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。

    假設上面的程序 flag 變量用 volatile 修飾


    volatile 內存語義的實現(xiàn)

    下面是 JMM 針對編譯器制定的 volatile 重排序規(guī)則表:

    為了實現(xiàn) volatile 的內存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。

    下面是基于保守策略的 JMM 內存屏障插入策略:

    • 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障。

    • 在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障。

    • 在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障。

    • 在每個 volatile 讀操作的后面插入一個 LoadStore 屏障。

    下面是保守策略下,volatile 寫操作 插入內存屏障后生成的指令序列示意圖:

    下面是在保守策略下,volatile 讀操作 插入內存屏障后生成的指令序列示意圖:

    上述 volatile 寫操作和 volatile 讀操作的內存屏障插入策略非常保守。在實際執(zhí)行時,只要不改變 volatile 寫-讀的內存語義,編譯器可以根據(jù)具體情況省略不必要的屏障。


    鎖釋放和獲取的內存語義

    當線程釋放鎖時,JMM 會把該線程對應的本地內存中的共享變量刷新到主內存中。

    當線程獲取鎖時,JMM 會把該線程對應的本地內存置為無效。從而使得被監(jiān)視器保護的臨界區(qū)代碼必須要從主內存中去讀取共享變量。


    鎖內存語義的實現(xiàn)

    借助 ReentrantLock 來講解,PS:后面專門講下這塊(ReentrantLock、Synchronized、公平鎖、非公平鎖、AQS等),可以看看大明哥的博客:[http://cmsblogs concurrent 包的實現(xiàn)

    如果我們仔細分析 concurrent 包的源代碼實現(xiàn),會發(fā)現(xiàn)一個通用化的實現(xiàn)模式:

    1. 首先,聲明共享變量為 volatile;

    2. 然后,使用 CAS 的原子條件更新來實現(xiàn)線程之間的同步;

    3. 同時,配合以 volatile 的讀/寫和 CAS 所具有的 volatile 讀和寫的內存語義來實現(xiàn)線程之間的通信。

    AQS,非阻塞數(shù)據(jù)結構和原子變量類(java.util.concurrent.atomic 包中的類),這些 concurrent 包中的基礎類都是使用這種模式來實現(xiàn)的,而 concurrent 包中的高層類又是依賴于這些基礎類來實現(xiàn)的。從整體來看,concurrent 包的實現(xiàn)示意圖如下:


    final

    對于 final 域,編譯器和處理器要遵守兩個重排序規(guī)則:

    1. 在構造函數(shù)內對一個 final 域的寫入,與隨后把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。

    2. 初次讀一個包含 final 域的對象的引用,與隨后初次讀這個 final 域,這兩個操作之間不能重排序。


    寫 final 域的重排序規(guī)則

    寫 final 域的重排序規(guī)則禁止把 final 域的寫重排序到構造函數(shù)之外。這個規(guī)則的實現(xiàn)包含下面2個方面:

    • JMM 禁止編譯器把 final 域的寫重排序到構造函數(shù)之外。

    • 編譯器會在 final 域的寫之后,構造函數(shù) return 之前,插入一個 StoreStore 屏障。這個屏障禁止處理器把 final 域的寫重排序到構造函數(shù)之外。


    讀 final 域的重排序規(guī)則

    在一個線程中,初次讀對象引用與初次讀該對象包含的 final 域,JMM 禁止處理器重排序這兩個操作(注意,這個規(guī)則僅僅針對處理器)。編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障。


    final 域是引用類型

    對于引用類型,寫 final 域的重排序規(guī)則對編譯器和處理器增加了如下約束:

    在構造函數(shù)內對一個 final 引用的對象的成員域的寫入,與隨后在構造函數(shù)外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。


    總結

    JMM,處理器內存模型與順序一致性內存模型之間的關系

    JMM 是一個語言級的內存模型,處理器內存模型是硬件級的內存模型,順序一致性內存模型是一個理論參考模型。下面是語言內存模型,處理器內存模型和順序一致性內存模型的強弱對比示意圖:


    JMM 的設計示意圖


    JMM 的內存可見性保證

    Java 程序的內存可見性保證按程序類型可以分為下列三類:

    1.zhisheng)里回復 面經(jīng)、ES、Flink、 Spring、Java、Kafka、監(jiān)控 等關鍵字可以查看更多關鍵字對應的文章


    Flink 實戰(zhàn)

    1、《從0到1學習Flink》—— Apache Flink 介紹

    2、《從0到1學習Flink》—— Mac 上搭建 Flink 1.6.0 環(huán)境并構建運行簡單程序入門

    3、《從0到1學習Flink》—— Flink 配置文件詳解

    4、《從0到1學習Flink》—— Data Source 介紹

    5、《從0到1學習Flink》—— 如何自定義 Data Source ?

    6、《從0到1學習Flink》—— Data Sink 介紹

    7、《從0到1學習Flink》—— 如何自定義 Data Sink ?

    8、《從0到1學習Flink》—— Flink Data transformation(轉換)

    9、《從0到1學習Flink》—— 介紹 Flink 中的 Stream Windows

    10、《從0到1學習Flink》—— Flink 中的幾種 Time 詳解

    11、《從0到1學習Flink》—— Flink 讀取 Kafka 數(shù)據(jù)寫入到 ElasticSearch

    12、《從0到1學習Flink》—— Flink 項目如何運行?

    13、《從0到1學習Flink》—— Flink 讀取 Kafka 數(shù)據(jù)寫入到 Kafka

    14、《從0到1學習Flink》—— Flink JobManager 高可用性配置

    15、《從0到1學習Flink》—— Flink parallelism 和 Slot 介紹

    16、《從0到1學習Flink》—— Flink 讀取 Kafka 數(shù)據(jù)批量寫入到 MySQL

    17、《從0到1學習Flink》—— Flink 讀取 Kafka 數(shù)據(jù)寫入到 RabbitMQ

    18、《從0到1學習Flink》—— 你上傳的 jar 包藏到哪里去了

    19、大數(shù)據(jù)“重磅炸彈”——實時計算框架 Flink

    20、《Flink 源碼解析》—— 源碼編譯運行

    21、為什么說流處理即未來?

    22、OPPO數(shù)據(jù)中臺之基石:基于Flink SQL構建實數(shù)據(jù)倉庫

    23、流計算框架 Flink 與 Storm 的性能對比

    24、Flink狀態(tài)管理和容錯機制介紹

    25、原理解析 | Apache Flink  Flink 源碼解析

    網(wǎng)站題目:深入理解 Java 內存模型
    文章分享:http://www.bm7419.com/news14/104314.html

    成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供做網(wǎng)站、域名注冊、網(wǎng)站設計、品牌網(wǎng)站制作、云服務器Google

    廣告

    聲明:本網(wǎng)站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經(jīng)允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)

    成都定制網(wǎng)站網(wǎng)頁設計