深入理解 Java 內(nèi)存模型

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

前提

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

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

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


通信

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

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

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


同步

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

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

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

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


Java 內(nèi)存模型的抽象

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

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

JMM 抽象示意圖:

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

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

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

步驟示意圖:

舉個例子:

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

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


重排序

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

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

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

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

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

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


處理器重排序

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

舉個例子:

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

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

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

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


內(nèi)存屏障指令

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


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

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

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

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

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

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


    as-if-serial 語義

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

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

    舉個例子:

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

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

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

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

    舉例:

    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ù)依賴關(guān)系,編譯器和處理器可以對這兩個操作重排序;操作 3 和操作 4 沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器也可以對這兩個操作重排序。

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

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

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

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

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

    在 順序一致性

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

    順序一致性內(nèi)存模型有兩大特性:

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

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

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

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

    舉個例子:

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

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

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

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

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

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

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

    請看下面的示例代碼:

    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}

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

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

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


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

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

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

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

    總線的工作機制:

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

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

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

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

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

    注意,在 JSR -133 之前的舊內(nèi)存模型中,一個 64 位 long/ double 型變量的讀/寫操作可以被拆分為兩個 32 位的讀/寫操作來執(zhí)行。從 JSR -133 內(nèi)存模型開始(即從JDK5開始),僅僅只允許把一個 64 位 long/ double 型變量的寫操作拆分為兩個 32 位的寫操作來執(zhí)行,任意的讀操作在JSR -133中都必須具有原子性(即任意讀操作必須要在單個讀事務(wù)中執(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++; // 復(fù)合(多個) volatile 變量的讀 /寫14 }15}

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

    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 { //普通方法調(diào)用13 long temp = get; //調(diào)用已同步的讀方法14 temp += 1L; //普通寫操作15 set(temp); //調(diào)用已同步的寫方法16 }17}

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

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

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

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

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

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


    volatile 寫-讀的內(nèi)存定義

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

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

    假設(shè)上面的程序 flag 變量用 volatile 修飾


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

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

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

    下面是基于保守策略的 JMM 內(nèi)存屏障插入策略:

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

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

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

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

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

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

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


    鎖釋放和獲取的內(nèi)存語義

    當線程釋放鎖時,JMM 會把該線程對應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。

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


    鎖內(nèi)存語義的實現(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 讀和寫的內(nèi)存語義來實現(xiàn)線程之間的通信。

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


    final

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

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

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


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

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

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

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


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

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


    final 域是引用類型

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

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


    總結(jié)

    JMM,處理器內(nèi)存模型與順序一致性內(nèi)存模型之間的關(guān)系

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


    JMM 的設(shè)計示意圖


    JMM 的內(nèi)存可見性保證

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

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


    Flink 實戰(zhàn)

    1、《從0到1學(xué)習(xí)Flink》—— Apache Flink 介紹

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

    3、《從0到1學(xué)習(xí)Flink》—— Flink 配置文件詳解

    4、《從0到1學(xué)習(xí)Flink》—— Data Source 介紹

    5、《從0到1學(xué)習(xí)Flink》—— 如何自定義 Data Source ?

    6、《從0到1學(xué)習(xí)Flink》—— Data Sink 介紹

    7、《從0到1學(xué)習(xí)Flink》—— 如何自定義 Data Sink ?

    8、《從0到1學(xué)習(xí)Flink》—— Flink Data transformation(轉(zhuǎn)換)

    9、《從0到1學(xué)習(xí)Flink》—— 介紹 Flink 中的 Stream Windows

    10、《從0到1學(xué)習(xí)Flink》—— Flink 中的幾種 Time 詳解

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

    12、《從0到1學(xué)習(xí)Flink》—— Flink 項目如何運行?

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

    14、《從0到1學(xué)習(xí)Flink》—— Flink JobManager 高可用性配置

    15、《從0到1學(xué)習(xí)Flink》—— Flink parallelism 和 Slot 介紹

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

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

    18、《從0到1學(xué)習(xí)Flink》—— 你上傳的 jar 包藏到哪里去了

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

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

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

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

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

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

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

    本文名稱:深入理解 Java 內(nèi)存模型
    文章鏈接:http://www.muchs.cn/news/104314.html

    成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供ChatGPT、App開發(fā)、網(wǎng)站設(shè)計公司、網(wǎng)站策劃虛擬主機、面包屑導(dǎo)航

    廣告

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

    外貿(mào)網(wǎng)站建設(shè)