文章首發(fā)于51CTO技術(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ā)程序。
更具體一點(diǎn)說,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í)行處理將圍繞解決這2個問題展開:
- 1 工作內(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ì)介紹。
- 2 指令重排序優(yōu)化 Java中重排序通常是編譯器或運(yùn)行時環(huán)境為了優(yōu)化程序性能而采取的對指令進(jìn)行重新排序執(zhí)行的一種手段。重排序分為兩類:編譯期重排序和運(yùn)行期重排序,分別對應(yīng)編譯時和運(yùn)行時環(huán)境。 同樣的,指令重排序不是隨意重排序,它需要滿足以下兩個條件: 1 在單線程環(huán)境下不能改變程序運(yùn)行的結(jié)果 即時編譯器(和處理器)需要保證程序能夠遵守 as-if-serial 屬性。通俗地說,就是在單線程情況下,要給程序一個順序執(zhí)行的假象。即經(jīng)過重排序的執(zhí)行結(jié)果要與順序執(zhí)行的結(jié)果保持一致。 2 存在數(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主要涉及2個步驟:
- 1 線程1把線程工作內(nèi)存中更新過的x的值刷新到主內(nèi)存中
- 2 線程2到主內(nèi)存中讀取線程1之前已更新過的x變量
從整體上看,這2個步驟是線程1在向線程2發(fā)消息,這個通信過程必須經(jīng)過主內(nèi)存。線程對變量的所有操作(讀取,賦值)都必須在工作內(nèi)存中進(jìn)行。不同線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來完成,實現(xiàn)各個線程提供共享變量的可見性。
內(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)識為一條線程獨(dú)占的狀態(tài)。
- unlock (解鎖) 作用于主內(nèi)存的變量,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
- read (讀取) 作用于主內(nèi)存的變量,它把一個變量的值從主內(nèi)存傳輸到線程的工作內(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ī)則
4.1 內(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)和線程間 線程內(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ī)則看起來有點(diǎn)繁瑣,但總結(jié)起來,是圍繞原子性、可見性、有序性特征建立。歸根究底,是為實現(xiàn)共享變量的在多個線程的工作內(nèi)存的數(shù)據(jù)一致性,多線程并發(fā),指令重排序優(yōu)化的環(huán)境中程序能如預(yù)期運(yùn)行。
4.2 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()方法的開始
4.3 內(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屏障之前的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)存屏障。
4.4 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 操作之一單獨(dú)出現(xiàn)。
- 規(guī)則3:不允許一個線程丟棄它的最近 assign 的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中。
- 規(guī)則4:不允許一個線程無原因的(沒有發(fā)生過任何 assign 操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中。
- 規(guī)則5:一個新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個未被初始化(load 或 assign )的變量。即就是對一個變量實施 use 和 store 操作之前,必須先執(zhí)行過了 load 或 assign 操作。
- 規(guī)則6:一個變量在同一個時刻只允許一條線程對其進(jìn)行 lock 操作,但 lock 操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行 lock 后,只有執(zhí)行相同次數(shù)的 unlock 操作,變量才會被解鎖。所以 lock 和 unlock 必須成對出現(xiàn)。
- 規(guī)則7:如果對一個變量執(zhí)行 lock 操作,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前需要重新執(zhí)行 load 或 assign 操作初始化變量的值。
- 規(guī)則8:如果一個變量事先沒有被 lock 操作鎖定,則不允許對它執(zhí)行 unlock 操作;也不允許去 unlock 一個被其他線程鎖定的變量。
- 規(guī)則9:對一個變量執(zhí)行 unlock 操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行 store 和 write 操作)
看起來這些規(guī)則有些繁瑣,其實也不難理解:
- 規(guī)則1、規(guī)則2 工作內(nèi)存中的共享變量作為主內(nèi)存的副本,主內(nèi)存變量的值同步到工作內(nèi)存需要read和load一起使用,工作內(nèi)存中的變量的值同步回主內(nèi)存需要store和write一起使用,這2組操作各自都是是一個固定的有序搭配,不允許單獨(dú)出現(xiàn)。
- 規(guī)則3、規(guī)則4 由于工作內(nèi)存中的共享變量是主內(nèi)存的副本,為保證數(shù)據(jù)一致性,當(dāng)工作內(nèi)存中的變量被字節(jié)碼引擎重新賦值,必須同步回主內(nèi)存。如果工作內(nèi)存的變量沒有被更新,不允許無原因同步回主內(nèi)存。
- 規(guī)則5 由于工作內(nèi)存中的共享變量是主內(nèi)存的副本,必須從主內(nèi)存誕生。
- 規(guī)則6、7、8、9 為了并發(fā)情況下安全使用變量,線程可以基于lock操作獨(dú)占主內(nèi)存中的變量,其他線程不允許使用或unlock該變量,直到變量被線程unlock。
4.5 volatile型變量的特殊規(guī)則
volatile的中文意思是不穩(wěn)定的,易變的,用volatile修飾變量是為了保證變量的可見性。
volatile的語義
volatile主要有下面2種語義
語義1 保證可見性
保證了不同線程對該變量操作的內(nèi)存可見性。
這里保證可見性是不等同于volatile變量并發(fā)操作的安全性,保證可見性具體一點(diǎn)解釋:
線程寫volatile變量的過程:
- 1 改變線程工作內(nèi)存中volatile變量副本的值
- 2 將改變后的副本的值從工作內(nèi)存刷新到主內(nèi)存
線程讀volatile變量的過程:
- 1 從主內(nèi)存中讀取volatile變量的最新值到線程的工作內(nèi)存中
- 2 從工作內(nèi)存中讀取volatile變量的副本
但是如果多個線程同時把更新后的變量值同時刷新回主內(nèi)存,可能導(dǎo)致得到的值不是預(yù)期結(jié)果:
舉個例子: 定義volatile int count = 0,2個線程同時執(zhí)行count++操作,每個線程都執(zhí)行500次,最終結(jié)果小于1000,原因是每個線程執(zhí)行count++需要以下3個步驟:
- 步驟1 線程從主內(nèi)存讀取最新的count的值
- 步驟2 執(zhí)行引擎把count值加1,并賦值給線程工作內(nèi)存
- 步驟3 線程工作內(nèi)存把count值保存到主內(nèi)存 有可能某一時刻2個線程在步驟1讀取到的值都是100,執(zhí)行完步驟2得到的值都是101,最后刷新了2次101保存到主內(nèi)存。
語義2 禁止進(jìn)行指令重排序
具體一點(diǎn)解釋,禁止重排序的規(guī)則如下:
- 當(dāng)程序執(zhí)行到 volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對后面的操作可見;在其后面的操作肯定還沒有進(jìn)行;
- 在進(jìn)行指令優(yōu)化時,不能將在對 volatile 變量訪問的語句放在其后面執(zhí)行,也不能把 volatile 變量后面的語句放到其前面執(zhí)行。
普通的變量僅僅會保證該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證賦值操作的順序與程序代碼中的執(zhí)行順序一致。
舉個例子:
volatile boolean initialized = false;
// 下面代碼線程A中執(zhí)行
// 讀取配置信息,當(dāng)讀取完成后將initialized設(shè)置為true以通知其他線程配置可用
doSomethingReadConfg();
initialized = true;
// 下面代碼線程B中執(zhí)行
// 等待initialized 為true,代表線程A已經(jīng)把配置信息初始化完成
while (!initialized) {
sleep();
}
// 使用線程A初始化好的配置信息
doSomethingWithConfig();
上面代碼中如果定義initialized變量時沒有使用volatile修飾,就有可能會由于指令重排序的優(yōu)化,導(dǎo)致線程A中最后一句代碼 "initialized = true" 在 “doSomethingReadConfg()” 之前被執(zhí)行,這樣會導(dǎo)致線程B中使用配置信息的代碼就可能出現(xiàn)錯誤,而volatile關(guān)鍵字就禁止重排序的語義可以避免此類情況發(fā)生。
volatile型變量實現(xiàn)原理
具體實現(xiàn)方式是在編譯期生成字節(jié)碼時,會在指令序列中增加內(nèi)存屏障來保證,下面是基于保守策略的JMM內(nèi)存屏障插入策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障。 該屏障除了保證了屏障之前的寫操作和該屏障之后的寫操作不能重排序,還會保證了volatile寫操作之前,任何的讀寫操作都會先于volatile被提交。
- 在每個volatile寫操作的后面插入一個StoreLoad屏障。 該屏障除了使volatile寫操作不會與之后的讀操作重排序外,還會刷新處理器緩存,使volatile變量的寫更新對其他線程可見。
- 在每個volatile讀操作的后面插入一個LoadLoad屏障。 該屏障除了使volatile讀操作不會與之前的寫操作發(fā)生重排序外,還會刷新處理器緩存,使volatile變量讀取的為最新值。
- 在每個volatile讀操作的后面插入一個LoadStore屏障。 該屏障除了禁止了volatile讀操作與其之后的任何寫操作進(jìn)行重排序,還會刷新處理器緩存,使其他線程volatile變量的寫更新對volatile讀操作的線程可見。
volatile型變量使用場景
總結(jié)起來,就是“一次寫入,到處讀取”,某一線程負(fù)責(zé)更新變量,其他線程只讀取變量(不更新變量),并根據(jù)變量的新值執(zhí)行相應(yīng)邏輯。例如狀態(tài)標(biāo)志位更新,觀察者模型變量值發(fā)布。
4.6 final型變量的特殊規(guī)則
我們知道,final成員變量必須在聲明的時候初始化或者在構(gòu)造器中初始化,否則就會報編譯錯誤。 final關(guān)鍵字的可見性是指:被final修飾的字段在聲明時或者構(gòu)造器中,一旦初始化完成,那么在其他線程無須同步就能正確看見final字段的值。這是因為一旦初始化完成,final變量的值立刻回寫到主內(nèi)存。
4.7 synchronized的特殊規(guī)則
通過 synchronized關(guān)鍵字包住的代碼區(qū)域,對數(shù)據(jù)的讀寫進(jìn)行控制:
- 讀數(shù)據(jù) 當(dāng)線程進(jìn)入到該區(qū)域讀取變量信息時,對數(shù)據(jù)的讀取也不能從工作內(nèi)存讀取,只能從內(nèi)存中讀取,保證讀到的是最新的值。
- 寫數(shù)據(jù) 在同步區(qū)內(nèi)對變量的寫入操作,在離開同步區(qū)時就將當(dāng)前線程內(nèi)的數(shù)據(jù)刷新到內(nèi)存中,保證更新的數(shù)據(jù)對其他線程的可見性。
4.8 long和double型變量的特殊規(guī)則
Java內(nèi)存模型要求lock、unlock、read、load、assign、use、store、write這8種操作都具有原子性,但是對于64位的數(shù)據(jù)類型(long和double),在模型中特別定義相對寬松的規(guī)定:允許虛擬機(jī)將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作分為2次32位的操作來進(jìn)行。也就是說虛擬機(jī)可選擇不保證64位數(shù)據(jù)類型的load、store、read和write這4個操作的原子性。由于這種非原子性,有可能導(dǎo)致其他線程讀到同步未完成的“32位的半個變量”的值。
不過實際開發(fā)中,Java內(nèi)存模型強(qiáng)烈建議虛擬機(jī)把64位數(shù)據(jù)的讀寫實現(xiàn)為具有原子性,目前各種平臺下的商用虛擬機(jī)都選擇把64位數(shù)據(jù)的讀寫操作作為原子操作來對待,因此我們在編寫代碼時一般不需要把用到的long和double變量專門聲明為volatile。
五、總結(jié)
由于Java內(nèi)存模型涉及系列規(guī)則,網(wǎng)上的文章大部分就是對這些規(guī)則進(jìn)行解析,但是很多沒有解釋為什么需要這些規(guī)則,這些規(guī)則的作用,其實這是不利于初學(xué)者學(xué)習(xí)的,容易繞進(jìn)去這些繁瑣規(guī)則不知所以然,下面談?wù)勎业囊稽c(diǎn)學(xué)習(xí)知識的個人體會:
學(xué)習(xí)知識的過程不是等同于只是理解知識和記憶知識,而是要對知識解決的問題的輸入和輸出建立連接,知識的本質(zhì)是解決問題,所以在學(xué)習(xí)之前要理解問題,理解這個問題要的輸出和輸出,而知識就是輸入到輸出的一個關(guān)系映射。知識的學(xué)習(xí)要結(jié)合大量的例子來理解這個映射關(guān)系,然后壓縮知識,華羅庚說過:“把一本書讀厚,然后再讀薄”,解釋的就是這個道理,先結(jié)合大量的例子理解知識,然后再壓縮知識。
以學(xué)習(xí)Java內(nèi)存模型為例:
- 理解問題,明確輸入輸出 首先理解Java內(nèi)存模型是什么,有什么用,解決什么問題
- 理解內(nèi)存模型系列協(xié)議 結(jié)合大量例子理解這些協(xié)議規(guī)則
- 壓縮知識 大量規(guī)則其實就是通過數(shù)據(jù)同步協(xié)議,保證內(nèi)存副本之間的數(shù)據(jù)一致性,同時防止重排序?qū)Τ绦虻挠绊憽?/li>