新聞中心
【稿件】這篇文章主要介紹模型產(chǎn)生的問題背景,解決的問題,處理思路,相關(guān)實現(xiàn)規(guī)則,環(huán)環(huán)相扣,希望讀者看完這篇文章后能對 Java 內(nèi)存模型體系產(chǎn)生一個相對清晰的理解,知其然知其所以然。

10年積累的成都網(wǎng)站設(shè)計、成都網(wǎng)站建設(shè)經(jīng)驗,可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識你,你也不認(rèn)識我。但先網(wǎng)站策劃后付款的網(wǎng)站建設(shè)流程,更有樟樹免費(fèi)網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
內(nèi)存模型產(chǎn)生背景
在介紹 Java 內(nèi)存模型之前,我們先了解一下物理計算機(jī)中的并發(fā)問題,理解這些問題可以搞清楚內(nèi)存模型產(chǎn)生的背景。
物理機(jī)遇到的并發(fā)問題與虛擬機(jī)中的情況有不少相似之處,物理機(jī)的解決方案對虛擬機(jī)的實現(xiàn)有相當(dāng)?shù)膮⒖家饬x。
物理機(jī)的并發(fā)問題
硬件的效率問題
計算機(jī)處理器處理絕大多數(shù)運(yùn)行任務(wù)都不可能只靠處理器“計算”就能完成,處理器至少需要與內(nèi)存交互,如讀取運(yùn)算數(shù)據(jù)、存儲運(yùn)算結(jié)果,這個 I/O 操作很難消除(無法僅靠寄存器完成所有運(yùn)算任務(wù))。
由于計算機(jī)的存儲設(shè)備與處理器的運(yùn)算速度有幾個數(shù)量級的差距,為了避免處理器等待緩慢的內(nèi)存完成讀寫操作,現(xiàn)代計算機(jī)系統(tǒng)通過加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存。
緩存作為內(nèi)存和處理器之間的緩沖:將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速運(yùn)行,當(dāng)運(yùn)算結(jié)束后再從緩存同步回內(nèi)存之中。
緩存一致性問題
基于高速緩存的存儲系統(tǒng)交互很好的解決了處理器與內(nèi)存速度的矛盾,但是也為計算機(jī)系統(tǒng)帶來更高的復(fù)雜度,因為引入了一個新問題:緩存一致性。
在多處理器的系統(tǒng)中(或者單處理器多核的系統(tǒng)),每個處理器(每個核)都有自己的高速緩存,而它們有共享同一主內(nèi)存(Main Memory)。
當(dāng)多個處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致。
為此,需要各個處理器訪問緩存時都遵循一些協(xié)議,在讀寫時要根據(jù)協(xié)議進(jìn)行操作,來維護(hù)緩存的一致性。
代碼亂序執(zhí)行優(yōu)化問題
為了使得處理器內(nèi)部的運(yùn)算單元盡量被充分利用,提高運(yùn)算效率,處理器可能會對輸入的代碼進(jìn)行亂序執(zhí)行。
處理器會在計算之后將亂序執(zhí)行的結(jié)果重組,亂序優(yōu)化可以保證在單線程下該執(zhí)行結(jié)果與順序執(zhí)行的結(jié)果是一致的,但不保證程序中各個語句計算的先后順序與輸入代碼中的順序一致。
亂序執(zhí)行技術(shù)是處理器為提高運(yùn)算速度而做出違背代碼原有順序的優(yōu)化。在單核時代,處理器保證做出的優(yōu)化不會導(dǎo)致執(zhí)行結(jié)果遠(yuǎn)離預(yù)期目標(biāo),但在多核環(huán)境下卻并非如此。
在多核環(huán)境下, 如果存在一個核的計算任務(wù)依賴另一個核計算任務(wù)的中間結(jié)果。
而且對相關(guān)數(shù)據(jù)讀寫沒做任何防護(hù)措施,那么其順序性并不能靠代碼的先后順序來保證,處理器最終得出的結(jié)果和我們邏輯得到的結(jié)果可能會大不相同。
以上圖為例進(jìn)行說明,CPU 的 core2 中的邏輯 B 依賴 core1 中的邏輯 A 先執(zhí)行:
- 正常情況下,邏輯 A 執(zhí)行完之后再執(zhí)行邏輯 B。
- 在處理器亂序執(zhí)行優(yōu)化情況下,有可能導(dǎo)致 flag 提前被設(shè)置為 true,導(dǎo)致邏輯 B 先于邏輯 A 執(zhí)行。
Java 內(nèi)存模型的組成分析
內(nèi)存模型概念
為了更好解決上面提到的系列問題,內(nèi)存模型被總結(jié)提出,我們可以把內(nèi)存模型理解為在特定操作協(xié)議下,對特定的內(nèi)存或高速緩存進(jìn)行讀寫訪問的過程抽象。
不同架構(gòu)的物理計算機(jī)可以有不一樣的內(nèi)存模型,Java 虛擬機(jī)也有自己的內(nèi)存模型。
Java 虛擬機(jī)規(guī)范中試圖定義一種 Java 內(nèi)存模型(Java Memory Model,簡稱 JMM)來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓 Java 程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果,不必因為不同平臺上的物理機(jī)的內(nèi)存模型的差異,對各平臺定制化開發(fā)程序。
更具體一點說,Java 內(nèi)存模型提出目標(biāo)在于,定義程序中各個變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。
此處的變量(Variables)與 Java 編程中所說的變量有所區(qū)別,它包括了實例字段、靜態(tài)字段和構(gòu)成數(shù)值對象的元素,但不包括局部變量與方法參數(shù),因為后者是線程私有的。
注:如果局部變量是一個 reference 類型,它引用的對象在 Java 堆中可被各個線程共享,但是 reference 本身在 Java 棧的局部變量表中,它是線程私有的。
Java 內(nèi)存模型的組成
主內(nèi)存
Java 內(nèi)存模型規(guī)定了所有變量都存儲在主內(nèi)存(Main Memory)中(此處的主內(nèi)存與介紹物理硬件的主內(nèi)存名字一樣,兩者可以互相類比,但此處僅是虛擬機(jī)內(nèi)存的一部分)。
工作內(nèi)存
每條線程都有自己的工作內(nèi)存(Working Memory,又稱本地內(nèi)存,可與前面介紹的處理器高速緩存類比),線程的工作內(nèi)存中保存了該線程使用到的變量的主內(nèi)存中的共享變量的副本拷貝。
工作內(nèi)存是 JMM 的一個抽象概念,并不真實存在。它涵蓋了緩存,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。
Java 內(nèi)存模型抽象示意圖如下:
JVM 內(nèi)存操作的并發(fā)問題
結(jié)合前面介紹的物理機(jī)的處理器處理內(nèi)存的問題,可以類比總結(jié)出 JVM 內(nèi)存操作的問題,下面介紹的 Java 內(nèi)存模型的執(zhí)行處理將圍繞解決這兩個問題展開。
工作內(nèi)存數(shù)據(jù)一致性
各個線程操作數(shù)據(jù)時會保存使用到的主內(nèi)存中的共享變量副本,當(dāng)多個線程的運(yùn)算任務(wù)都涉及同一個共享變量時,將導(dǎo)致各自的共享變量副本不一致,如果真的發(fā)生這種情況,數(shù)據(jù)同步回主內(nèi)存以誰的副本數(shù)據(jù)為準(zhǔn)?
Java 內(nèi)存模型主要通過一系列的數(shù)據(jù)同步協(xié)議、規(guī)則來保證數(shù)據(jù)的一致性,后面再詳細(xì)介紹。
指令重排序優(yōu)化
Java 中重排序通常是編譯器或運(yùn)行時環(huán)境為了優(yōu)化程序性能而采取的對指令進(jìn)行重新排序執(zhí)行的一種手段。
重排序分為兩類:編譯期重排序和運(yùn)行期重排序,分別對應(yīng)編譯時和運(yùn)行時環(huán)境。
同樣的,指令重排序不是隨意重排序,它需要滿足以下兩個條件:
- 在單線程環(huán)境下不能改變程序運(yùn)行的結(jié)果。即時編譯器(和處理器)需要保證程序能夠遵守 as-if-serial 屬性。
通俗地說,就是在單線程情況下,要給程序一個順序執(zhí)行的假象。即經(jīng)過重排序的執(zhí)行結(jié)果要與順序執(zhí)行的結(jié)果保持一致。
- 存在數(shù)據(jù)依賴關(guān)系的不允許重排序。
多線程環(huán)境下,如果線程處理邏輯之間存在依賴關(guān)系,有可能因為指令重排序?qū)е逻\(yùn)行結(jié)果與預(yù)期不同,后面再展開 Java 內(nèi)存模型如何解決這種情況。
Java 內(nèi)存間的交互操作
在理解 Java 內(nèi)存模型的系列協(xié)議、特殊規(guī)則之前,我們先理解 Java 中內(nèi)存間的交互操作。
交互操作流程
為了更好理解內(nèi)存的交互操作,以線程通信為例,我們看看具體如何進(jìn)行線程間值的同步:
線程 1 和線程 2 都有主內(nèi)存中共享變量 x 的副本,初始時,這 3 個內(nèi)存中 x 的值都為 0。
線程 1 中更新 x 的值為 1 之后同步到線程 2 主要涉及兩個步驟:
-
線程 1 把線程工作內(nèi)存中更新過的 x 的值刷新到主內(nèi)存中。
-
線程 2 到主內(nèi)存中讀取線程 1 之前已更新過的 x 變量。
從整體上看,這兩個步驟是線程 1 在向線程 2 發(fā)消息,這個通信過程必須經(jīng)過主內(nèi)存。
JMM 通過控制主內(nèi)存與每個線程本地內(nèi)存之間的交互,來為各個線程提供共享變量的可見性。
內(nèi)存交互的基本操作
關(guān)于主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存之類的實現(xiàn)細(xì)節(jié),Java 內(nèi)存模型中定義了下面 8 種操作來完成。
虛擬機(jī)實現(xiàn)時必須保證下面介紹的每種操作都是原子的,不可再分的(對于 double 和 long 型的變量來說,load、store、read、和 write 操作在某些平臺上允許有例外)。
8 種基本操作,如下圖:
- lock (鎖定) ,作用于主內(nèi)存的變量,它把一個變量標(biāo)識為一條線程獨占的狀態(tài)。
- unlock (解鎖) ,作用于主內(nèi)存的變量,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
- read (讀取) ,作用于主內(nèi)存的變量,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的 load 動作使用。
- load (載入) ,作用于工作內(nèi)存的變量,它把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
- use (使用) ,作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個需要使用到變量的值的字節(jié)碼指令時就會執(zhí)行這個操作。
- assign (賦值) ,作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
- store (存儲) ,作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中,以便隨后 write 操作使用。
- write (寫入) ,作用于主內(nèi)存的變量,它把 Store 操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
Java 內(nèi)存模型運(yùn)行規(guī)則
內(nèi)存交互基本操作的 3 個特性
在介紹內(nèi)存交互的具體的 8 種基本操作之前,有必要先介紹一下操作的 3 個特性。
Java 內(nèi)存模型是圍繞著在并發(fā)過程中如何處理這 3 個特性來建立的,這里先給出定義和基本實現(xiàn)的簡單介紹,后面會逐步展開分析。
原子性(Atomicity)
原子性,即一個操作或者多個操作要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。
即使在多個線程一起執(zhí)行的時候,一個操作一旦開始,就不會被其他線程所干擾。
可見性(Visibility)
可見性是指當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
正如上面“交互操作流程”中所說明的一樣,JMM 是通過在線程 1 變量工作內(nèi)存修改后將新值同步回主內(nèi)存,線程 2 在變量讀取前從主內(nèi)存刷新變量值,這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)可見性。
有序性(Ordering)
有序性規(guī)則表現(xiàn)在以下兩種場景:
- 線程內(nèi),從某個線程的角度看方法的執(zhí)行,指令會按照一種叫“串行”(as-if-serial)的方式執(zhí)行,此種方式已經(jīng)應(yīng)用于順序編程語言。
- 線程間,這個線程“觀察”到其他線程并發(fā)地執(zhí)行非同步的代碼時,由于指令重排序優(yōu)化,任何代碼都有可能交叉執(zhí)行。
唯一起作用的約束是:對于同步方法,同步塊(synchronized 關(guān)鍵字修飾)以及 volatile 字段的操作仍維持相對有序。
Java 內(nèi)存模型的一系列運(yùn)行規(guī)則看起來有點繁瑣,但總結(jié)起來,是圍繞原子性、可見性、有序性特征建立。
歸根究底,是為實現(xiàn)共享變量的在多個線程的工作內(nèi)存的數(shù)據(jù)一致性,多線程并發(fā),指令重排序優(yōu)化的環(huán)境中程序能如預(yù)期運(yùn)行。
happens-before 關(guān)系
介紹系列規(guī)則之前,首先了解一下 happens-before 關(guān)系:用于描述下 2 個操作的內(nèi)存可見性。如果操作 A happens-before 操作 B,那么 A 的結(jié)果對 B 可見。
happens-before 關(guān)系的分析需要分為單線程和多線程的情況:
- 單線程下的 happens-before,字節(jié)碼的先后順序天然包含 happens-before 關(guān)系:因為單線程內(nèi)共享一份工作內(nèi)存,不存在數(shù)據(jù)一致性的問題。
在程序控制流路徑中靠前的字節(jié)碼 happens-before 靠后的字節(jié)碼,即靠前的字節(jié)碼執(zhí)行完之后操作結(jié)果對靠后的字節(jié)碼可見。
- 然而,這并不意味著前者一定在后者之前執(zhí)行。實際上,如果后者不依賴前者的運(yùn)行結(jié)果,那么它們可能會被重排序。
多線程下的 happens-before,多線程由于每個線程有共享變量的副本,如果沒有對共享變量做同步處理,線程 1 更新執(zhí)行操作 A 共享變量的值之后,線程 2 開始執(zhí)行操作 B,此時操作 A 產(chǎn)生的結(jié)果對操作 B 不一定可見。
為了方便程序開發(fā),Java 內(nèi)存模型實現(xiàn)了下述支持 happens-before 關(guān)系的操作:
- 程序次序規(guī)則,一個線程內(nèi),按照代碼順序,書寫在前面的操作 happens-before 書寫在后面的操作。
- 鎖定規(guī)則,一個 unLock 操作 happens-before 后面對同一個鎖的 lock 操作。
- volatile 變量規(guī)則,對一個變量的寫操作 happens-before 后面對這個變量的讀操作。
- 傳遞規(guī)則,如果操作 A happens-before 操作 B,而操作 B 又 happens-before 操作 C,則可以得出操作 A happens-before 操作 C。
- 線程啟動規(guī)則,Thread 對象的 start() 方法 happens-before 此線程的每個一個動作。
- 線程中斷規(guī)則,對線程 interrupt() 方法的調(diào)用 happens-before 被中斷線程的代碼檢測到中斷事件的發(fā)生。
- 線程終結(jié)規(guī)則,線程中所有的操作都 happens-before 線程的終止檢測,我們可以通過 Thread.join() 方法結(jié)束、Thread.isAlive() 的返回值手段檢測到線程已經(jīng)終止執(zhí)行。
- 對象終結(jié)規(guī)則,一個對象的初始化完成 happens-before 它的 finalize() 方法的開始。
內(nèi)存屏障
Java 中如何保證底層操作的有序性和可見性?可以通過內(nèi)存屏障。
內(nèi)存屏障是被插入兩個 CPU 指令之間的一種指令,用來禁止處理器指令發(fā)生重排序(像屏障一樣),從而保障有序性的。
另外,為了達(dá)到屏障的效果,它也會使處理器寫入、讀取值之前,將主內(nèi)存的值寫入高速緩存,清空無效隊列,從而保障可見性。
舉個例子說明:
- Store1;
- Store2;
- Load1;
- StoreLoad; //內(nèi)存屏障
- Store3;
- Load2;
- Load3;
對于上面的一組 CPU 指令(Store 表示寫入指令,Load 表示讀取指令,StoreLoad 代表寫讀內(nèi)存屏障),StoreLoad 屏障之前的 Store 指令無法與 StoreLoad 屏障之后的 Load 指令進(jìn)行交換位置,即重排序。
但是 StoreLoad 屏障之前和之后的指令是可以互換位置的,即 Store1 可以和 Store2 互換,Load2 可以和 Load3 互換。
常見有 4 種屏障:
- LoadLoad 屏障:對于這樣的語句 Load1;LoadLoad;Load2,在 Load2 及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證 Load1 要讀取的數(shù)據(jù)被讀取完畢。
- StoreStore 屏障:對于這樣的語句 Store1;StoreStore;Store2,在 Store2 及后續(xù)寫入操作執(zhí)行前,保證 Store1 的寫入操作對其他處理器可見。
- LoadStore 屏障:對于這樣的語句 Load1;LoadStore;Store2,在 Store2 及后續(xù)寫入操作被執(zhí)行前,保證 Load1 要讀取的數(shù)據(jù)被讀取完畢。
- StoreLoad 屏障:對于這樣的語句 Store1;StoreLoad;Load2,在 Load2 及后續(xù)所有讀取操作執(zhí)行前,保證 Store1 的寫入對所有處理器可見。它的開銷是四種屏障中***的(沖刷寫緩沖器,清空無效化隊列)。
在大多數(shù)處理器的實現(xiàn)中,這個屏障是個***屏障,兼具其他三種內(nèi)存屏障的功能。
Java 中對內(nèi)存屏障的使用在一般的代碼中不太容易見到,常見的有 volatile 和 synchronized 關(guān)鍵字修飾的代碼塊(后面再展開介紹),還可以通過 Unsafe 這個類來使用內(nèi)存屏障。
8 種操作同步的規(guī)則
JMM 在執(zhí)行前面介紹 8 種基本操作時,為了保證內(nèi)存間數(shù)據(jù)一致性,JMM 中規(guī)定需要滿足以下規(guī)則:
- 規(guī)則 1:如果要把一個變量從主內(nèi)存中復(fù)制到工作內(nèi)存,就需要按順序的執(zhí)行 read 和 load 操作,如果把變量從工作內(nèi)存中同步回主內(nèi)存中,就要按順序的執(zhí)行 store 和 write 操作。
- 但 Java 內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行。
- 規(guī)則 2:不允許 read 和 load、store 和 write 操作之一單獨出現(xiàn)。
- 規(guī)則 3:不允許一個線程丟棄它的最近 assign 的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中。
- 規(guī)則 4:不允許一個線程無原因的(沒有發(fā)生過任何 assign 操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中。
- http://www.fisionsoft.com.cn/article/djhjdej.html


咨詢
建站咨詢
