不會(huì)Java內(nèi)存模型,就先別扯什么熟悉并發(fā)編程
本文公眾號(hào)來(lái)源:JavaKeeper作者:大猿帥本文已收錄至我的GitHub
??? 面試官:
- 你知道 Java 內(nèi)存模型 JMM 嗎?那你知道它的三大特性嗎?
- Java 是如何解決指令重排問(wèn)題的?
- 既然CPU有緩存一致性協(xié)議(MESI),為什么 JMM 還需要volatile關(guān)鍵字?
前兩天看到同學(xué)和我顯擺他們公司配的電腦多好多好,我默默打開(kāi)了自己的電腦,酷睿 i7-4770,也不是不夠用嘛,4 核 8 線程的 CPU,也是杠杠的。
扯這玩意干啥,Em~~~~
介紹 Java 內(nèi)存模型之前,先溫習(xí)下計(jì)算機(jī)硬件內(nèi)存模型
硬件內(nèi)存架構(gòu)
計(jì)算機(jī)在執(zhí)行程序的時(shí)候,每條指令都是在 CPU 中執(zhí)行的,而執(zhí)行的時(shí)候,又免不了要和數(shù)據(jù)打交道。而計(jì)算機(jī)上面的數(shù)據(jù),是存放在主存當(dāng)中的,也就是計(jì)算機(jī)的物理內(nèi)存。
計(jì)算機(jī)硬件架構(gòu)簡(jiǎn)易圖:
我們以多核 CPU 為例,每個(gè)CPU 核都包含一組 「CPU 寄存器」,這些寄存器本質(zhì)上是在 CPU 內(nèi)存中。CPU 在這些寄存器上執(zhí)行操作的速度要比在主內(nèi)存(RAM)中執(zhí)行的速度快得多。
因?yàn)?strong>CPU速率高, 內(nèi)存速率慢,為了讓存儲(chǔ)體系可以跟上CPU的速度,所以中間又加上 Cache 層,就是我們說(shuō)的 「CPU 高速緩存」。
CPU多級(jí)緩存
由于CPU的運(yùn)算速度遠(yuǎn)遠(yuǎn)超越了1級(jí)緩存的數(shù)據(jù)I\O能力,CPU廠商又引入了多級(jí)的緩存結(jié)構(gòu)。通常L1、L2 是每個(gè)CPU 核有一個(gè),L3 是多個(gè)核共用一個(gè)。
Cache Line
Cache又是由很多個(gè)「緩存行」(Cache line) 組成的。Cache line 是 Cache 和 RAM 交換數(shù)據(jù)的最小單位。
Cache 存儲(chǔ)數(shù)據(jù)是固定大小為單位的,稱為一個(gè)Cache entry,這個(gè)單位稱為Cache line或Cache block。給定Cache 容量大小和 Cache line size 的情況下,它能存儲(chǔ)的條目個(gè)數(shù)(number of cache entries)就是固定的。因?yàn)镃ache 是固定大小的,所以它從主內(nèi)存獲取數(shù)據(jù)也是固定大小。對(duì)于X86來(lái)講,是 64Bytes。對(duì)于ARM來(lái)講,較舊的架構(gòu)的Cache line是32Bytes,但一次內(nèi)存訪存只訪問(wèn)一半的數(shù)據(jù)也不太合適,所以它經(jīng)常是一次填兩個(gè) Cache line,叫做 double fill。
緩存的工作原理
這里的緩存的工作原理和我們項(xiàng)目中用 memcached、redis 做常用數(shù)據(jù)的緩存層是一個(gè)道理。
當(dāng) CPU 要讀取一個(gè)數(shù)據(jù)時(shí),首先從緩存中查找,如果找到就立即讀取并送給CPU處理;如果沒(méi)有找到,就去內(nèi)存中讀取并送給 CPU 處理,同時(shí)把這個(gè)數(shù)據(jù)所在的數(shù)據(jù)塊(就是我們上邊說(shuō)的 Cache block)調(diào)入緩存中,即把臨近的共 64 Byte 的數(shù)據(jù)也一同載入,因?yàn)榕R近的數(shù)據(jù)在將來(lái)被訪問(wèn)的可能性更大,可以使得以后對(duì)整塊數(shù)據(jù)的讀取都從緩存中進(jìn)行,不必再調(diào)用內(nèi)存。
這就增加了CPU讀取緩存的命中率(Cache hit)了。
計(jì)算機(jī)層級(jí)存儲(chǔ)
計(jì)算機(jī)存儲(chǔ)系統(tǒng)是有層次結(jié)構(gòu)的,類似一個(gè)金字塔,頂層的寄存器讀寫速度較高,但是空間較小。底層的讀寫速度較低,但是空間較大
緩存一致性
既然每個(gè)核中都有單獨(dú)的緩存,那我的 4 核 8 線程 CPU 處理主內(nèi)存數(shù)據(jù)的時(shí)候,不就會(huì)出現(xiàn)數(shù)據(jù)不一致問(wèn)題了嗎?
為了解決這個(gè)問(wèn)題,先后有過(guò)兩種方法:總線鎖機(jī)制和緩存鎖機(jī)制。
總線鎖就是使用 CPU 提供的一個(gè)LOCK#信號(hào),當(dāng)一個(gè)處理器在總線上輸出此信號(hào),其他處理器的請(qǐng)求將被阻塞,那么該處理器就可以獨(dú)占共享鎖。這樣就保證了數(shù)據(jù)一致性。
但是總線鎖開(kāi)銷太大,我們需要控制鎖的粒度,所以又有了緩存鎖,核心就是“緩存一致性協(xié)議”,不同的 CPU 硬件廠商實(shí)現(xiàn)方式稍有不同,有MSI、MESI、MOSI等。
代碼亂序執(zhí)行優(yōu)化
為了使得處理器內(nèi)部的運(yùn)算單元盡量被充分利用,提高運(yùn)算效率,處理器可能會(huì)對(duì)輸入的代碼進(jìn)行「亂序執(zhí)行」(Out-Of-Order Execution),處理器會(huì)在計(jì)算之后將亂序執(zhí)行的結(jié)果重組,亂序優(yōu)化可以保證在單線程下該執(zhí)行結(jié)果與順序執(zhí)行的結(jié)果是一致的,但不保證程序中各個(gè)語(yǔ)句計(jì)算的先后順序與輸入代碼中的順序一致。
亂序執(zhí)行技術(shù)是處理器為提高運(yùn)算速度而做出違背代碼原有順序的優(yōu)化。在單核時(shí)代,處理器保證做出的優(yōu)化不會(huì)導(dǎo)致執(zhí)行結(jié)果遠(yuǎn)離預(yù)期目標(biāo),但在多核環(huán)境下卻并非如此。
多核環(huán)境下, 如果存在一個(gè)核的計(jì)算任務(wù)依賴另一個(gè)核的計(jì)算任務(wù)的中間結(jié)果,而且對(duì)相關(guān)數(shù)據(jù)讀寫沒(méi)做任何防護(hù)措施,那么其順序性并不能靠代碼的先后順序來(lái)保證,處理器最終得出的結(jié)果和我們邏輯得到的結(jié)果可能會(huì)大不相同。
編譯器指令重排
除了上述由處理器和緩存引起的亂序之外,現(xiàn)代編譯器同樣提供了亂序優(yōu)化。之所以出現(xiàn)編譯器亂序優(yōu)化其根本原因在于處理器每次只能分析一小塊指令,但編譯器卻能在很大范圍內(nèi)進(jìn)行代碼分析,從而做出更優(yōu)的策略,充分利用處理器的亂序執(zhí)行功能。
內(nèi)存屏障
盡管我們看到亂序執(zhí)行初始目的是為了提高效率,但是它看來(lái)其好像在這多核時(shí)代不盡人意,其中的某些”自作聰明”的優(yōu)化導(dǎo)致多線程程序產(chǎn)生各種各樣的意外。因此有必要存在一種機(jī)制來(lái)消除亂序執(zhí)行帶來(lái)的壞影響,也就是說(shuō)應(yīng)該允許程序員顯式的告訴處理器對(duì)某些地方禁止亂序執(zhí)行。這種機(jī)制就是所謂內(nèi)存屏障。不同架構(gòu)的處理器在其指令集中提供了不同的指令來(lái)發(fā)起內(nèi)存屏障,對(duì)應(yīng)在編程語(yǔ)言當(dāng)中就是提供特殊的關(guān)鍵字來(lái)調(diào)用處理器相關(guān)的指令,JMM里我們?cè)偬接憽?/p>
Java內(nèi)存模型
Java 內(nèi)存模型即 Java Memory Model,簡(jiǎn)稱 JMM。
這里的內(nèi)存模型可不是 JVM 里的運(yùn)行時(shí)數(shù)據(jù)區(qū)。
「內(nèi)存模型」可以理解為在特定操作協(xié)議下,對(duì)特定的內(nèi)存或高速緩存進(jìn)行讀寫訪問(wèn)的過(guò)程抽象。
不同架構(gòu)的物理計(jì)算機(jī)可以有不一樣的內(nèi)存模型,Java虛擬機(jī)也有自己的內(nèi)存模型。
Java虛擬機(jī)規(guī)范中試圖定義一種「 Java 內(nèi)存模型」來(lái)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)讓 Java 程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果,不必因?yàn)椴煌脚_(tái)上的物理機(jī)的內(nèi)存模型的差異,對(duì)各平臺(tái)定制化開(kāi)發(fā)程序。
Java 內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問(wèn)規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。這里的變量與我們寫 Java 代碼中的變量不同,它包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但不包括局部變量和方法參數(shù),因?yàn)樗麄兪蔷€程私有的,不會(huì)被共享。
JMM 組成
主內(nèi)存:Java 內(nèi)存模型規(guī)定了所有變量都存儲(chǔ)在主內(nèi)存(Main Memory)中(此處的主內(nèi)存與物理硬件的主內(nèi)存RAM 名字一樣,兩者可以互相類比,但此處僅是虛擬機(jī)內(nèi)存的一部分)。
工作內(nèi)存:每條線程都有自己的工作內(nèi)存(Working Memory,又稱本地內(nèi)存,可與CPU高速緩存類比),線程的工作內(nèi)存中保存了該線程使用到的主內(nèi)存中的共享變量的副本拷貝。線程對(duì)變量的所有操作都必須在工作內(nèi)存進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。工作內(nèi)存是 JMM 的一個(gè)抽象概念,并不真實(shí)存在。
JMM 與 JVM 內(nèi)存結(jié)構(gòu)
JMM 與 Java 內(nèi)存區(qū)域中的堆、棧、方法區(qū)等并不是同一個(gè)層次的內(nèi)存劃分,兩者基本沒(méi)有關(guān)系。如果一定要勉強(qiáng)對(duì)應(yīng),那從變量、主內(nèi)存、工作內(nèi)存的定義看,主內(nèi)存主要對(duì)應(yīng) Java 堆中的對(duì)象實(shí)例數(shù)據(jù)部分,工作內(nèi)存則對(duì)應(yīng)虛擬機(jī)棧的部分區(qū)域(與上圖對(duì)應(yīng)著看哈)。
JMM 與計(jì)算機(jī)內(nèi)存結(jié)構(gòu)
Java 內(nèi)存模型和硬件內(nèi)存體系結(jié)構(gòu)也沒(méi)有什么關(guān)系。硬件內(nèi)存體系結(jié)構(gòu)不區(qū)分棧和堆。在硬件上,線程棧和堆都位于主內(nèi)存中。線程棧和堆的一部分有時(shí)可能出現(xiàn)在高速緩存和CPU寄存器中。如下圖所示:
當(dāng)對(duì)象和變量可以存儲(chǔ)在計(jì)算機(jī)中不同的內(nèi)存區(qū)域時(shí),這就可能會(huì)出現(xiàn)某些問(wèn)題。兩個(gè)主要問(wèn)題是:
- 線程更新(寫)到共享變量的可見(jiàn)性
- 讀取、檢查和寫入共享變量時(shí)的競(jìng)爭(zhēng)條件
可見(jiàn)性問(wèn)題(Visibility of Shared Objects)
如果兩個(gè)或多個(gè)線程共享一個(gè)對(duì)象,則一個(gè)線程對(duì)共享對(duì)象的更新可能對(duì)其他線程不可見(jiàn)(當(dāng)然可以用 Java 提供的關(guān)鍵字 volatile)。假設(shè)共享對(duì)象最初存儲(chǔ)在主內(nèi)存中。在 CPU 1上運(yùn)行的線程將共享對(duì)象讀入它的CPU緩存后修改,但是還沒(méi)來(lái)得及即刷新回主內(nèi)存,這時(shí)其他 CPU 上運(yùn)行的線程就不會(huì)看到共享對(duì)象的更改。這樣,每個(gè)線程都可能以自己的線程結(jié)束,就出現(xiàn)了可見(jiàn)性問(wèn)題,如下
競(jìng)爭(zhēng)條件(Race Conditions)
這個(gè)其實(shí)就是我們常說(shuō)的原子問(wèn)題。
如果兩個(gè)或多個(gè)線程共享一個(gè)對(duì)象,并且多個(gè)線程更新該共享對(duì)象中的變量,則可能出現(xiàn)競(jìng)爭(zhēng)條件。
想象一下,如果線程A將一個(gè)共享對(duì)象的變量讀入到它的CPU緩存中。此時(shí),線程B執(zhí)行相同的操作,但是進(jìn)入不同的CPU緩存。現(xiàn)在線程A執(zhí)行 +1 操作,線程B也這樣做。現(xiàn)在該變量增加了兩次,在每個(gè)CPU緩存中一次。
如果這些增量是按順序執(zhí)行的,則變量結(jié)果會(huì)是3,并將原始值+ 2寫回主內(nèi)存。但是,這兩個(gè)增量是同時(shí)執(zhí)行的,沒(méi)有適當(dāng)?shù)耐健2还軐⒛膫€(gè)線程的結(jié)構(gòu)寫回主內(nèi)存,更新后的值只比原始值高1,顯然是有問(wèn)題的。如下(當(dāng)然可以用 Java 提供的關(guān)鍵字 Synchronized)
JMM 特性
JMM 就是用來(lái)解決如上問(wèn)題的。JMM是圍繞著并發(fā)過(guò)程中如何處理可見(jiàn)性、原子性和有序性這 3 個(gè) 特征建立起來(lái)的
可見(jiàn)性:可見(jiàn)性是指當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改。Java 中的 volatile、synchronzied、final 都可以實(shí)現(xiàn)可見(jiàn)性
原子性:即一個(gè)操作或者多個(gè)操作,要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷,要么就都不執(zhí)行。即使在多個(gè)線程一起執(zhí)行的時(shí)候,一個(gè)操作一旦開(kāi)始,就不會(huì)被其他線程所干擾。
有序性:
計(jì)算機(jī)在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排,一般分為以下 3 種

單線程環(huán)境里確保程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致;
處理器在進(jìn)行重排序時(shí)必須要考慮指令之間的數(shù)據(jù)依賴性;
多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在,兩個(gè)線程中使用的變量能否保證一致性是無(wú)法確定的,結(jié)果無(wú)法預(yù)測(cè)
內(nèi)存之間的交互操作
關(guān)于主內(nèi)存和工作內(nèi)存之間具體的交互協(xié)議,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存之類的實(shí)現(xiàn)細(xì)節(jié),Java 內(nèi)存模型中定義了 8 種 操作來(lái)完成,虛擬機(jī)實(shí)現(xiàn)必須保證每一種操作都是原子的、不可再拆分的(double和long類型例外)
- lock(鎖定):作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)。
- unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定。
- read(讀取):作用于主內(nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用。
- load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
- use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
- store(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write操作使用。
- write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
如果需要把一個(gè)變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要順序地執(zhí)行 read 和 load 操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要順序地執(zhí)行 store 和 write 操作。注意,Java 內(nèi)存模型只要求上述兩個(gè)操作必須按順序執(zhí)行,而沒(méi)有保證是連續(xù)執(zhí)行。也就是說(shuō) read 與 load 之間、store 與write 之間是可插入其他指令的,如對(duì)主內(nèi)存中的變量 a、b 進(jìn)行訪問(wèn)時(shí),一種可能出現(xiàn)順序是 read a、read b、load b、load a。
除此之外,Java 內(nèi)存模型還規(guī)定了在執(zhí)行上述 8 種基本操作時(shí)必須滿足如下規(guī)則
- 不允許 read 和 load、store 和 write 操作之一單獨(dú)出現(xiàn),即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者從工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)。
- 不允許一個(gè)線程丟棄它的最近的 assign 操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存。
- 不允許一個(gè)線程無(wú)原因地(沒(méi)有發(fā)生過(guò)任何 assign 操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存。
- 一個(gè)新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load 或 assign)的變量,換句話說(shuō),就是對(duì)一個(gè)變量實(shí)施 use、store 操作之前,必須先執(zhí)行過(guò)了 assign 和 load 操作。
- 一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作,但 lock 操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行 lock 后,只有執(zhí)行相同次數(shù)的 unlock 操作,變量才會(huì)被解鎖。
- 如果對(duì)一個(gè)變量執(zhí)行 lock 操作,那將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行 load 或 assign 操作初始化變量的值。
- 如果一個(gè)變量事先沒(méi)有被 lock 操作鎖定,那就不允許對(duì)它執(zhí)行 unlock 操作,也不允許去 unlock 一個(gè)被其他線程鎖定住的變量。
- 對(duì)一個(gè)變量執(zhí)行 unlock 操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行 store、write 操作)。
long 和 double 型變量的特殊規(guī)則
Java 內(nèi)存模型要求 lock,unlock,read,load,assign,use,store,write 這 8 個(gè)操作都具有原子性,但對(duì)于64 位的數(shù)據(jù)類型( long 或 double),在模型中定義了一條相對(duì)寬松的規(guī)定,允許虛擬機(jī)將沒(méi)有被 volatile 修飾的 64 位數(shù)據(jù)的讀寫操作劃分為兩次 32 位的操作來(lái)進(jìn)行,即允許虛擬機(jī)實(shí)現(xiàn)選擇可以不保證 64 位數(shù)據(jù)類型的load,store,read,write 這 4 個(gè)操作的原子性,即 long 和 double 的非原子性協(xié)定。
如果多線程的情況下double 或 long 類型并未聲明為 volatile,可能會(huì)出現(xiàn)“半個(gè)變量”的數(shù)值,也就是既非原值,也非修改后的值。
雖然 Java 規(guī)范允許上面的實(shí)現(xiàn),但商用虛擬機(jī)中基本都采用了原子性的操作,因此在日常使用中幾乎不會(huì)出現(xiàn)讀取到“半個(gè)變量”的情況,so,這個(gè)了解下就行。
先行發(fā)生原則
先行發(fā)生(happens-before)是 Java 內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系,如果操作A 先行發(fā)生于操作B,那么A的結(jié)果對(duì)B可見(jiàn)。happens-before關(guān)系的分析需要分為單線程和多線程的情況:
- 單線程下的 happens-before 字節(jié)碼的先后順序天然包含happens-before關(guān)系:因?yàn)閱尉€程內(nèi)共享一份工作內(nèi)存,不存在數(shù)據(jù)一致性的問(wèn)題。在程序控制流路徑中靠前的字節(jié)碼 happens-before 靠后的字節(jié)碼,即靠前的字節(jié)碼執(zhí)行完之后操作結(jié)果對(duì)靠后的字節(jié)碼可見(jiàn)。然而,這并不意味著前者一定在后者之前執(zhí)行。實(shí)際上,如果后者不依賴前者的運(yùn)行結(jié)果,那么它們可能會(huì)被重排序。
- 多線程下的 happens-before 多線程由于每個(gè)線程有共享變量的副本,如果沒(méi)有對(duì)共享變量做同步處理,線程1更新執(zhí)行操作A共享變量的值之后,線程2開(kāi)始執(zhí)行操作B,此時(shí)操作A產(chǎn)生的結(jié)果對(duì)操作B不一定可見(jiàn)。
為了方便程序開(kāi)發(fā),Java 內(nèi)存模型實(shí)現(xiàn)了下述的先行發(fā)生關(guān)系:
- 程序次序規(guī)則: 一個(gè)線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。
- 管程鎖定規(guī)則: 一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。
- volatile變量規(guī)則: 對(duì)一個(gè)變量的寫操作 happens-before 后面對(duì)這個(gè)變量的讀操作。
- 傳遞規(guī)則: 如果操作A 先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A 先行發(fā)生于操作C。
- 線程啟動(dòng)規(guī)則: Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作。
- 線程中斷規(guī)則: 對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生。
- 線程終結(jié)規(guī)則: 線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過(guò)
Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行。 - 對(duì)象終結(jié)規(guī)則: 一個(gè)對(duì)象的初始化完成先行發(fā)生于它的
finalize()方法的開(kāi)始
內(nèi)存屏障
上邊的一系列操作保證了數(shù)據(jù)一致性,Java中如何保證底層操作的有序性和可見(jiàn)性?可以通過(guò)內(nèi)存屏障。
內(nèi)存屏障是被插入兩個(gè) CPU 指令之間的一種指令,用來(lái)禁止處理器指令發(fā)生重排序(像屏障一樣),從而保障有序性的。另外,為了達(dá)到屏障的效果,它也會(huì)使處理器寫入、讀取值之前,將主內(nèi)存的值寫入高速緩存,清空無(wú)效隊(duì)列,從而保障可見(jiàn)性。
eg:
Store1;
Store2;
Load1;
StoreLoad; //內(nèi)存屏障
Store3;
Load2;
Load3;
對(duì)于上面的一組 CPU 指令(Store表示寫入指令,Load表示讀取指令),StoreLoad 屏障之前的 Store 指令無(wú)法與StoreLoad 屏障之后的 Load 指令進(jìn)行交換位置,即重排序。但是 StoreLoad 屏障之前和之后的指令是可以互換位置的,即 Store1 可以和 Store2 互換,Load2 可以和 Load3 互換。
常見(jiàn)的 4 種屏障
- LoadLoad 屏障:對(duì)于這樣的語(yǔ)句 Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問(wèn)前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
- StoreStore 屏障:對(duì)于這樣的語(yǔ)句 Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對(duì)其它處理器可見(jiàn)。
- LoadStore 屏障:對(duì)于這樣的語(yǔ)句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被執(zhí)行前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
- StoreLoad 屏障:對(duì)于這樣的語(yǔ)句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對(duì)所有處理器可見(jiàn)。它的開(kāi)銷是四種屏障中最大的(沖刷寫緩沖器,清空無(wú)效化隊(duì)列)。在大多數(shù)處理器的實(shí)現(xiàn)中,這個(gè)屏障也被稱為全能屏障,兼具其它三種內(nèi)存屏障的功能。
Java 中對(duì)內(nèi)存屏障的使用在一般的代碼中不太容易見(jiàn)到,常見(jiàn)的有 volatile 和 synchronized 關(guān)鍵字修飾的代碼塊,還可以通過(guò) Unsafe 這個(gè)類來(lái)使用內(nèi)存屏障。(下一章扯扯這些)
Java 內(nèi)存模型就是通過(guò)定義的這些來(lái)解決可見(jiàn)性、原子性和有序性的。
各類知識(shí)點(diǎn)總結(jié)
下面的文章都有對(duì)應(yīng)的原創(chuàng)精美PDF,在持續(xù)更新中,可以來(lái)找我催更~
- 92頁(yè)的Mybatis
- 129頁(yè)的多線程
- 141頁(yè)的Servlet
- 158頁(yè)的JSP
- 76頁(yè)的集合
- 64頁(yè)的JDBC
- 105頁(yè)的數(shù)據(jù)結(jié)構(gòu)和算法
- 142頁(yè)的Spring
- 58頁(yè)的過(guò)濾器和監(jiān)聽(tīng)器
- 30頁(yè)的HTTP
- 42頁(yè)的SpringMVC
- Hibernate
- AJAX
- Redis
- ......
掃碼或者微信搜Java3y 免費(fèi)領(lǐng)取原創(chuàng)思維導(dǎo)圖、精美PDF。在公眾號(hào)回復(fù)「888」領(lǐng)取,PDF內(nèi)容純手打有任何不懂歡迎來(lái)問(wèn)我。
原創(chuàng)電子書
原創(chuàng)思維導(dǎo)圖

![]() |
|



