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

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

前提

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

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

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


通信

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

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

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


同步

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

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

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

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


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

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

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

JMM 抽象示意圖:

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

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

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

步驟示意圖:

舉個(gè)例子:

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

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


重排序

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

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

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

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

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

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


處理器重排序

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

舉個(gè)例子:

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

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

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

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


內(nèi)存屏障指令

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


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

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

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

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

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

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


    as-if-serial 語義

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

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

    舉個(gè)例子:

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

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

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

    在計(jì)算機(jī)中,軟件技術(shù)和硬件技術(shù)有一個(gè)共同的目標(biāo):在不改變程序執(zhí)行結(jié)果的前提下,盡可能的開發(fā)并行度。編譯器和處理器遵從這一目標(biāo),從 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)系,編譯器和處理器可以對(duì)這兩個(gè)操作重排序;操作 3 和操作 4 沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器也可以對(duì)這兩個(gè)操作重排序。

    1、當(dāng)操作 1 和操作 2 重排序時(shí),可能會(huì)產(chǎn)生什么效果?

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

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

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

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

    在 順序一致性

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

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

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

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

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

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

    舉個(gè)例子:

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

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

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

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

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

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

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

    請看下面的示例代碼:

    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 方法。這是一個(gè)正確同步的多線程程序。根據(jù)JMM規(guī)范,該程序的執(zhí)行結(jié)果將與該程序在順序一致性模型中的執(zhí)行結(jié)果相同。下面是該程序在兩個(gè)內(nèi)存模型中的執(zhí)行時(shí)序?qū)Ρ葓D:

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

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


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

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

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

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

    總線的工作機(jī)制:

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

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

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

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

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

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


    Volatile

    Volatile 特性

    舉個(gè)例子:

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

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

    1public class VolatileTest {2 long a = 1L; // 64 位的 long 型普通變量34 public synchronized void set(long l) { //對(duì)單個(gè)普通變量的寫用同一個(gè)鎖同步5 a = l;6 }78 public synchronized long get { //對(duì)單個(gè)普通變量的讀用同一個(gè)鎖同步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}

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

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

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

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

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

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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

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


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

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

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


    鎖內(nèi)存語義的實(shí)現(xiàn)

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

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

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

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

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

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


    final

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

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

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


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

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

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

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


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

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


    final 域是引用類型

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

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


    總結(jié)

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

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


    JMM 的設(shè)計(jì)示意圖


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

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

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


    Flink 實(shí)戰(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)建運(yùn)行簡單程序入門

    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 項(xiàng)目如何運(yùn)行?

    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ù)“重磅炸彈”——實(shí)時(shí)計(jì)算框架 Flink

    20、《Flink 源碼解析》—— 源碼編譯運(yùn)行

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

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

    23、流計(jì)算框架 Flink 與 Storm 的性能對(duì)比

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

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

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

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

    廣告

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

    成都定制網(wǎng)站網(wǎng)頁設(shè)計(jì)