国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
Java內(nèi)存模型原理,你真的理解嗎?

最近重新學(xué)習(xí)了一遍《深入學(xué)習(xí) Java 虛擬機》,把之前 Java 內(nèi)存模型中模糊的知識重新梳理了一遍。


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

內(nèi)存模型產(chǎn)生背景

在介紹 Java 內(nèi)存模型之前,我們先了解一下物理計算機中的并發(fā)問題,理解這些問題可以搞清楚內(nèi)存模型產(chǎn)生的背景。

物理機遇到的并發(fā)問題與虛擬機中的情況有不少相似之處,物理機的解決方案對虛擬機的實現(xiàn)有相當(dāng)?shù)膮⒖家饬x。

物理機的并發(fā)問題

硬件的效率問題

計算機處理器處理絕大多數(shù)運行任務(wù)都不可能只靠處理器“計算”就能完成,處理器至少需要與內(nèi)存交互,如讀取運算數(shù)據(jù)、存儲運算結(jié)果,這個 I/O 操作很難消除(無法僅靠寄存器完成所有運算任務(wù))。

由于計算機的存儲設(shè)備與處理器的運算速度有幾個數(shù)量級的差距,為了避免處理器等待緩慢的內(nèi)存完成讀寫操作,現(xiàn)代計算機系統(tǒng)通過加入一層讀寫速度盡可能接近處理器運算速度的高速緩存。

緩存作為內(nèi)存和處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運算能快速運行,當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存之中。



緩存一致性問題

基于高速緩存的存儲系統(tǒng)交互很好的解決了處理器與內(nèi)存速度的矛盾,但是也為計算機系統(tǒng)帶來更高的復(fù)雜度,因為引入了一個新問題:緩存一致性。

在多處理器的系統(tǒng)中(或者單處理器多核的系統(tǒng)),每個處理器(每個核)都有自己的高速緩存,而它們有共享同一主內(nèi)存(Main Memory)。

當(dāng)多個處理器的運算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致。

為此,需要各個處理器訪問緩存時都遵循一些協(xié)議,在讀寫時要根據(jù)協(xié)議進行操作,來維護緩存的一致性。



代碼亂序執(zhí)行優(yōu)化問題

為了使得處理器內(nèi)部的運算單元盡量被充分利用,提高運算效率,處理器可能會對輸入的代碼進行亂序執(zhí)行。

處理器會在計算之后將亂序執(zhí)行的結(jié)果重組,亂序優(yōu)化可以保證在單線程下該執(zhí)行結(jié)果與順序執(zhí)行的結(jié)果是一致的,但不保證程序中各個語句計算的先后順序與輸入代碼中的順序一致。



亂序執(zhí)行技術(shù)是處理器為提高運算速度而做出違背代碼原有順序的優(yōu)化。在單核時代,處理器保證做出的優(yōu)化不會導(dǎo)致執(zhí)行結(jié)果遠離預(yù)期目標,但在多核環(huán)境下卻并非如此。

在多核環(huán)境下, 如果存在一個核的計算任務(wù)依賴另一個核計算任務(wù)的中間結(jié)果。

而且對相關(guān)數(shù)據(jù)讀寫沒做任何防護措施,那么其順序性并不能靠代碼的先后順序來保證,處理器最終得出的結(jié)果和我們邏輯得到的結(jié)果可能會大不相同。



以上圖為例進行說明,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)存或高速緩存進行讀寫訪問的過程抽象。

不同架構(gòu)的物理計算機可以有不一樣的內(nèi)存模型,Java 虛擬機也有自己的內(nèi)存模型。

Java 虛擬機規(guī)范中試圖定義一種 Java 內(nèi)存模型(Java Memory Model,簡稱 JMM)來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓 Java 程序在各種平臺下都能達到一致的內(nèi)存訪問效果,不必因為不同平臺上的物理機的內(nèi)存模型的差異,對各平臺定制化開發(fā)程序。

更具體一點說,Java 內(nèi)存模型提出目標在于,定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細節(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)存名字一樣,兩者可以互相類比,但此處僅是虛擬機內(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é)合前面介紹的物理機的處理器處理內(nèi)存的問題,可以類比總結(jié)出 JVM 內(nèi)存操作的問題,下面介紹的 Java 內(nèi)存模型的執(zhí)行處理將圍繞解決這兩個問題展開。

工作內(nèi)存數(shù)據(jù)一致性

各個線程操作數(shù)據(jù)時會保存使用到的主內(nèi)存中的共享變量副本,當(dāng)多個線程的運算任務(wù)都涉及同一個共享變量時,將導(dǎo)致各自的共享變量副本不一致,如果真的發(fā)生這種情況,數(shù)據(jù)同步回主內(nèi)存以誰的副本數(shù)據(jù)為準?

Java 內(nèi)存模型主要通過一系列的數(shù)據(jù)同步協(xié)議、規(guī)則來保證數(shù)據(jù)的一致性,后面再詳細介紹。

指令重排序優(yōu)化

Java 中重排序通常是編譯器或運行時環(huán)境為了優(yōu)化程序性能而采取的對指令進行重新排序執(zhí)行的一種手段。

重排序分為兩類:編譯期重排序和運行期重排序,分別對應(yīng)編譯時和運行時環(huán)境。

同樣的,指令重排序不是隨意重排序,它需要滿足以下兩個條件:

  • 在單線程環(huán)境下不能改變程序運行的結(jié)果。即時編譯器(和處理器)需要保證程序能夠遵守 as-if-serial 屬性。

通俗地說,就是在單線程情況下,要給程序一個順序執(zhí)行的假象。即經(jīng)過重排序的執(zhí)行結(jié)果要與順序執(zhí)行的結(jié)果保持一致。

  • 存在數(shù)據(jù)依賴關(guān)系的不允許重排序。

多線程環(huán)境下,如果線程處理邏輯之間存在依賴關(guān)系,有可能因為指令重排序?qū)е逻\行結(jié)果與預(yù)期不同,后面再展開 Java 內(nèi)存模型如何解決這種情況。

Java 內(nèi)存間的交互操作

在理解 Java 內(nèi)存模型的系列協(xié)議、特殊規(guī)則之前,我們先理解 Java 中內(nèi)存間的交互操作。

交互操作流程

為了更好理解內(nèi)存的交互操作,以線程通信為例,我們看看具體如何進行線程間值的同步:



線程 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)細節(jié),Java 內(nèi)存模型中定義了下面 8 種操作來完成。

虛擬機實現(xiàn)時必須保證下面介紹的每種操作都是原子的,不可再分的(對于 double 和 long 型的變量來說,load、store、read、和 write 操作在某些平臺上允許有例外)。



8 種基本操作,如下圖:

  • lock (鎖定) ,作用于主內(nèi)存的變量,它把一個變量標識為一條線程獨占的狀態(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)虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時就會執(zhí)行這個操作。
  • assign (賦值) ,作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
  • store (存儲) ,作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中,以便隨后 write 操作使用。
  • write (寫入) ,作用于主內(nèi)存的變量,它把 Store 操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。

Java 內(nèi)存模型運行規(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)存模型的一系列運行規(guī)則看起來有點繁瑣,但總結(jié)起來,是圍繞原子性、可見性、有序性特征建立。

歸根究底,是為實現(xiàn)共享變量的在多個線程的工作內(nèi)存的數(shù)據(jù)一致性,多線程并發(fā),指令重排序優(yōu)化的環(huán)境中程序能如預(yù)期運行。

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í)行。實際上,如果后者不依賴前者的運行結(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ā)生重排序(像屏障一樣),從而保障有序性的。

另外,為了達到屏障的效果,它也會使處理器寫入、讀取值之前,將主內(nèi)存的值寫入高速緩存,清空無效隊列,從而保障可見性。

舉個例子說明:

Store1; 
Store2;
Load1;
StoreLoad; //內(nèi)存屏障
Store3;
Load2;
Load3;

對于上面的一組 CPU 指令(Store 表示寫入指令,Load 表示讀取指令,StoreLoad 代表寫讀內(nèi)存屏障),StoreLoad 屏障之前的 Store 指令無法與 StoreLoad 屏障之后的 Load 指令進行交換位置,即重排序。

但是 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)存中。
  • 規(guī)則 5:一個新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個未被初始化(load 或 assign )的變量。
  • 即對一個變量實施 use 和 store 操作之前,必須先執(zhí)行過了 load 或 assign 操作。
  • 規(guī)則 6:一個變量在同一個時刻只允許一條線程對其進行 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 組操作各自都是一個固定的有序搭配,不允許單獨出現(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 操作獨占主內(nèi)存中的變量,其他線程不允許使用或 unlock 該變量,直到變量被線程 unlock。

volatile 型變量的特殊規(guī)則

volatile 的中文意思是不穩(wěn)定的,易變的,用 volatile 修飾變量是為了保證變量的可見性。

volatile 的語義

volatile 主要有下面 2 種語義:

  • 保證可見性
  • 禁止進行指令重排序

保證可見性,保證了不同線程對該變量操作的內(nèi)存可見性。這里保證可見性不等同于 volatile 變量并發(fā)操作的安全性,保證可見性具體一點解釋:

  • 線程對變量進行修改之后,要立刻回寫到主內(nèi)存。
  • 線程對變量讀取的時候,要從主內(nèi)存中讀,而不是從線程的工作內(nèi)存。

但是如果多個線程同時把更新后的變量值同時刷新回主內(nèi)存,可能導(dǎo)致得到的值不是預(yù)期結(jié)果。

舉個例子:定義 volatile int count = 0,2 個線程同時執(zhí)行 count++ 操作,每個線程都執(zhí)行 500 次,最終結(jié)果小于 1000。

原因是每個線程執(zhí)行 count++ 需要以下 3 個步驟:

  • 線程從主內(nèi)存讀取最新的 count 的值。
  • 執(zhí)行引擎把 count 值加 1,并賦值給線程工作內(nèi)存。
  • 線程工作內(nèi)存把 count 值保存到主內(nèi)存。

有可能某一時刻 2 個線程在步驟 1 讀取到的值都是 100,執(zhí)行完步驟 2 得到的值都是 101,最后刷新了 2 次 101 保存到主內(nèi)存。

禁止進行指令重排序,具體一點解釋,禁止重排序的規(guī)則如下:

  • 當(dāng)程序執(zhí)行到 volatile 變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經(jīng)進行,且結(jié)果已經(jīng)對后面的操作可見;在其后面的操作肯定還沒有進行。
  • 在進行指令優(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 讀操作與其之后的任何寫操作進行重排序,還會刷新處理器緩存,使其他線程 volatile 變量的寫更新對 volatile 讀操作的線程可見。

volatile 型變量使用場景

總結(jié)起來,就是“一次寫入,到處讀取”,某一線程負責(zé)更新變量,其他線程只讀取變量(不更新變量),并根據(jù)變量的新值執(zhí)行相應(yīng)邏輯。例如狀態(tài)標志位更新,觀察者模型變量值發(fā)布。

final 型變量的特殊規(guī)則

我們知道,final 成員變量必須在聲明的時候初始化或者在構(gòu)造器中初始化,否則就會報編譯錯誤。

final 關(guān)鍵字的可見性是指:被 final 修飾的字段在聲明時或者構(gòu)造器中,一旦初始化完成,那么在其他線程無須同步就能正確看見 final 字段的值。這是因為一旦初始化完成,final 變量的值立刻回寫到主內(nèi)存。

synchronized 的特殊規(guī)則

通過 synchronized 關(guān)鍵字包住的代碼區(qū)域,對數(shù)據(jù)的讀寫進行控制:

  • 讀數(shù)據(jù),當(dāng)線程進入到該區(qū)域讀取變量信息時,對數(shù)據(jù)的讀取也不能從工作內(nèi)存讀取,只能從內(nèi)存中讀取,保證讀到的是最新的值。
  • 寫數(shù)據(jù),在同步區(qū)內(nèi)對變量的寫入操作,在離開同步區(qū)時就將當(dāng)前線程內(nèi)的數(shù)據(jù)刷新到內(nèi)存中,保證更新的數(shù)據(jù)對其他線程的可見性。

long 和 double 型變量的特殊規(guī)則

Java 內(nèi)存模型要求 lock、unlock、read、load、assign、use、store、write 這 8 種操作都具有原子性。

但是對于 64 位的數(shù)據(jù)類型(long 和 double),在模型中特別定義相對寬松的規(guī)定:允許虛擬機將沒有被 volatile 修飾的 64 位數(shù)據(jù)的讀寫操作分為 2 次 32 位的操作來進行。

也就是說虛擬機可選擇不保證 64 位數(shù)據(jù)類型的 load、store、read 和 write 這 4 個操作的原子性。

由于這種非原子性,有可能導(dǎo)致其他線程讀到同步未完成的“32 位的半個變量”的值。

不過實際開發(fā)中,Java 內(nèi)存模型強烈建議虛擬機把 64 位數(shù)據(jù)的讀寫實現(xiàn)為具有原子性。

目前各種平臺下的商用虛擬機都選擇把 64 位數(shù)據(jù)的讀寫操作作為原子操作來對待,因此我們在編寫代碼時一般不需要把用到的 long 和 double 變量專門聲明為 volatile。

總結(jié)

由于 Java 內(nèi)存模型涉及系列規(guī)則,網(wǎng)上的文章大部分就是對這些規(guī)則進行解析,但是很多沒有解釋為什么需要這些規(guī)則,這些規(guī)則的作用。

其實這是不利于初學(xué)者學(xué)習(xí)的,容易繞進這些繁瑣規(guī)則不知所以然,下面談?wù)勎业囊稽c學(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>

參考文章:

  • 《深入學(xué)習(xí)Java虛擬機》
  • 深入拆解Java虛擬機
  • Java核心技術(shù)36講
  • Synchronization and the Java Memory Model ——Doug Lea
  • 深入理解 Java 內(nèi)存模型
  • Java內(nèi)存屏障和可見性
  • 內(nèi)存屏障與synchronized、volatile的原理

陳彩華(caison),從事服務(wù)端開發(fā),善于系統(tǒng)設(shè)計、優(yōu)化重構(gòu)、線上問題排查工作,主要開發(fā)語言是 Java,微信號:hua1881375。

【51CTO原創(chuàng)稿件,合作站點轉(zhuǎn)載請注明原文作者和出處為51CTO.com】

本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
Java并發(fā)JVM內(nèi)存模型處理器從而實現(xiàn)安全且高效的多線程功能
JMM和底層實現(xiàn)原理
【死磕Java并發(fā)】—–Java內(nèi)存模型之分析volatile
一個volatile跟面試官扯了半個小時
Java程序員面試必備:Volatile全方位解析
一道關(guān)于Java并發(fā)的面試題
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服