理解Java記憶體模型

貝聊科技發表於2018-11-19
文章首發於51CTO技術棧公眾號
作者 陳彩華
文章轉載交流請聯絡 caison@aliyun.com
複製程式碼

最近重新學習一遍《深入學習Java虛擬機器》,把之前Java記憶體模型中模糊的知識重新梳理一遍,這篇文章主要介紹模型產生的問題背景,解決的問題,處理思路,相關實現規則,環環相扣,希望讀者看完這篇文章後能對Java記憶體模型體系產生一個相對清晰的理解,知其然而知其所以然。

1 記憶體模型產生背景

在介紹Java記憶體模型之前,我們先了解一下物理計算機中的併發問題,理解這些問題可以搞清楚記憶體模型產生的背景。物理機遇到的併發問題與虛擬機器中的情況有不少相似之處,物理機的解決方案對虛擬機器的實現有相當的參考意義。

物理機的併發問題

  • 硬體的效率問題

計算機處理器處理絕大多數執行任務都不可能只靠處理器“計算”就能完成,處理器至少需要與記憶體互動,如讀取運算資料、儲存運算結果,這個I/O操作很難消除(無法僅靠暫存器完成所有運算任務)。

由於計算機的儲存裝置與處理器的運算速度有幾個數量級的差距,為了避免處理器等待緩慢的記憶體讀寫操作完成,現代計算機系統通過加入一層讀寫速度儘可能接近處理器運算速度的快取記憶體。快取作為記憶體和處理器之間的緩衝:將運算需要使用到的資料複製到快取中,讓運算能快速執行,當運算結束後再從快取同步回記憶體之中。

CPU快取記憶體

  • 快取一致性問題

基於快取記憶體的儲存系統互動很好地解決了處理器與記憶體速度的矛盾,但是也為計算機系統帶來更高的複雜度,因為引入了一個新問題:快取一致性。

在多處理器的系統中(或者單處理器多核的系統),每個處理器(每個核)都有自己的快取記憶體,而它們有共享同一主記憶體(Main Memory)。當多個處理器的運算任務都涉及同一塊主記憶體區域時,將可能導致各自的快取資料不一致。 為此,需要各個處理器訪問快取時都遵循一些協議,在讀寫時要根據協議進行操作,來維護快取的一致性。

快取一致性

  • 程式碼亂序執行優化問題

為了使得處理器內部的運算單元儘量被充分利用,提高運算效率,處理器可能會對輸入的程式碼進行亂序執行,處理器會在計算之後將亂序執行的結果重組,亂序優化可以保證在單執行緒下該執行結果與順序執行的結果是一致的,但不保證程式中各個語句計算的先後順序與輸入程式碼中的順序一致。

程式碼執行亂序優化

亂序執行技術是處理器為提高運算速度而做出違背程式碼原有順序的優化。在單核時代,處理器保證做出的優化不會導致執行結果遠離預期目標,但在多核環境下卻並非如此。

多核環境下, 如果存在一個核的計算任務依賴另一個核 計的算任務的中間結果,而且對相關資料讀寫沒做任何防護措施,那麼其順序性並不能靠程式碼的先後順序來保證,處理器最終得出的結果和我們邏輯得到的結果可能會大不相同。

程式碼亂序執行優化的問題

以上圖為例進行說明:CPU的core2中的邏輯B依賴core1中的邏輯A先執行

  • 正常情況下,邏輯A執行完之後再執行邏輯B。
  • 在處理器亂序執行優化情況下,有可能導致flag提前被設定為true,導致邏輯B先於邏輯A執行。

2 Java記憶體模型的組成分析

記憶體模型概念

為了更好解決上面提到系列問題,記憶體模型被總結提出,我們可以把記憶體模型理解為在特定操作協議下,對特定的記憶體或快取記憶體進行讀寫訪問的過程抽象

不同架構的物理計算機可以有不一樣的記憶體模型,Java虛擬機器也有自己的記憶體模型。Java虛擬機器規範中試圖定義一種Java記憶體模型(Java Memory Model,簡稱JMM)來遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的記憶體訪問效果,不必因為不同平臺上的物理機的記憶體模型的差異,對各平臺定製化開發程式。

更具體一點說,Java記憶體模型提出目標在於,定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。此處的變數(Variables)與Java程式設計中所說的變數有所區別,它包括了例項欄位、靜態欄位和構成數值物件的元素,但不包括區域性變數與方法引數,因為後者是執行緒私有的。(如果區域性變數是一個reference型別,它引用的物件在Java堆中可被各個執行緒共享,但是reference本身在Java棧的區域性變數表中,它是執行緒私有的)。

Java記憶體模型的組成

  • 主記憶體 Java記憶體模型規定了所有變數都儲存在主記憶體(Main Memory)中(此處的主記憶體與介紹物理硬體的主記憶體名字一樣,兩者可以互相類比,但此處僅是虛擬機器記憶體的一部分)。

  • 工作記憶體 每條執行緒都有自己的工作記憶體(Working Memory,又稱本地記憶體,可與前面介紹的處理器快取記憶體類比),執行緒的工作記憶體中儲存了該執行緒使用到的變數的主記憶體中的共享變數的副本拷貝。工作記憶體是 JMM 的一個抽象概念,並不真實存在。它涵蓋了快取,寫緩衝區,暫存器以及其他的硬體和編譯器優化。

Java記憶體模型抽象示意圖如下:

Java記憶體模型抽象示意圖

JVM記憶體操作的併發問題

結合前面介紹的物理機的處理器處理記憶體的問題,可以類比總結出JVM記憶體操作的問題,下面介紹的Java記憶體模型的執行處理將圍繞解決這2個問題展開:

  • 1 工作記憶體資料一致性 各個執行緒運算元據時會儲存使用到的主記憶體中的共享變數副本,當多個執行緒的運算任務都涉及同一個共享變數時,將導致各自的的共享變數副本不一致,如果真的發生這種情況,資料同步回主記憶體以誰的副本資料為準? Java記憶體模型主要通過一系列的資料同步協議、規則來保證資料的一致性,後面再詳細介紹。

  • 2 指令重排序優化 Java中重排序通常是編譯器或執行時環境為了優化程式效能而採取的對指令進行重新排序執行的一種手段。重排序分為兩類:編譯期重排序和執行期重排序,分別對應編譯時和執行時環境。 同樣的,指令重排序不是隨意重排序,它需要滿足以下兩個條件:

    • 1 在單執行緒環境下不能改變程式執行的結果 即時編譯器(和處理器)需要保證程式能夠遵守 as-if-serial 屬性。通俗地說,就是在單執行緒情況下,要給程式一個順序執行的假象。即經過重排序的執行結果要與順序執行的結果保持一致。
    • 2 存在資料依賴關係的不允許重排序

多執行緒環境下,如果執行緒處理邏輯之間存在依賴關係,有可能因為指令重排序導致執行結果與預期不同,後面再展開Java記憶體模型如何解決這種情況。

3 Java記憶體間的互動操作

在理解Java記憶體模型的系列協議、特殊規則之前,我們先理解Java中記憶體間的互動操作。

互動操作流程

為了更好理解記憶體的互動操作,以執行緒通訊為例,我們看看具體如何進行執行緒間值的同步:

執行緒間互動操作

執行緒1和執行緒2都有主記憶體中共享變數x的副本,初始時,這3個記憶體中x的值都為0。執行緒1中更新x的值為1之後同步到執行緒2主要涉及2個步驟:

  • 1 執行緒1把執行緒工作記憶體中更新過的x的值重新整理到主記憶體中
  • 2 執行緒2到主記憶體中讀取執行緒1之前已更新過的x變數

從整體上看,這2個步驟是執行緒1在向執行緒2發訊息,這個通訊過程必須經過主記憶體。執行緒對變數的所有操作(讀取,賦值)都必須在工作記憶體中進行。不同執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成,實現各個執行緒提供共享變數的可見性。

記憶體互動的基本操作

關於主記憶體與工作記憶體之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體之類的實現細節,Java記憶體模型中定義了下面介紹8種操作來完成。

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

8種基本操作

8種基本操作

  • lock (鎖定) 作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。
  • unlock (解鎖) 作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
  • read (讀取) 作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。
  • load (載入) 作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
  • use (使用) 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值得位元組碼指令時就會執行這個操作。
  • assign (賦值) 作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
  • store (儲存) 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後write操作使用。
  • write (寫入) 作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。

4 Java記憶體模型執行規則

4.1 記憶體互動基本操作的3個特性

在介紹記憶體的互動的具體的8種基本操作之前,有必要先介紹一下操作的3個特性,Java記憶體模型是圍繞著在併發過程中如何處理這3個特性來建立的,這裡先給出定義和基本實現的簡單介紹,後面會逐步展開分析。

  • 原子性(Atomicity) 即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。即使在多個執行緒一起執行的時候,一個操作一旦開始,就不會被其他執行緒所干擾。

  • 可見性(Visibility) 是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。 正如上面“互動操作流程”中所說明的一樣,JMM是通過線上程1變數工作記憶體修改後將新值同步回主記憶體,執行緒2在變數讀取前從主記憶體重新整理變數值,這種依賴主記憶體作為傳遞媒介的方式來實現可見性。

  • 有序性(Ordering) 有序性規則表現在以下兩種場景: 執行緒內和執行緒間

    • 執行緒內 從某個執行緒的角度看方法的執行,指令會按照一種叫“序列”(as-if-serial)的方式執行,此種方式已經應用於順序程式語言。
    • 執行緒間 這個執行緒“觀察”到其他執行緒併發地執行非同步的程式碼時,由於指令重排序優化,任何程式碼都有可能交叉執行。唯一起作用的約束是:對於同步方法,同步塊(synchronized關鍵字修飾)以及volatile欄位的操作仍維持相對有序。

Java記憶體模型的一系列執行規則看起來有點繁瑣,但總結起來,是圍繞原子性、可見性、有序性特徵建立。歸根究底,是為實現共享變數的在多個執行緒的工作記憶體的資料一致性,多執行緒併發,指令重排序優化的環境中程式能如預期執行。

4.2 happens-before關係

介紹系列規則之前,首先了解一下happens-before關係:用於描述下2個操作的記憶體可見性:如果操作A happens-before 操作B,那麼A的結果對B可見。happens-before關係的分析需要分為單執行緒和多執行緒的情況:

  • 單執行緒下的 happens-before 位元組碼的先後順序天然包含happens-before關係:因為單執行緒內共享一份工作記憶體,不存在資料一致性的問題。 在程式控制流路徑中靠前的位元組碼 happens-before 靠後的位元組碼,即靠前的位元組碼執行完之後操作結果對靠後的位元組碼可見。然而,這並不意味著前者一定在後者之前執行。實際上,如果後者不依賴前者的執行結果,那麼它們可能會被重排序。

  • 多執行緒下的 happens-before 多執行緒由於每個執行緒有共享變數的副本,如果沒有對共享變數做同步處理,執行緒1更新執行操作A共享變數的值之後,執行緒2開始執行操作B,此時操作A產生的結果對操作B不一定可見。

為了方便程式開發,Java記憶體模型實現了下述支援happens-before關係的操作:

  • 程式次序規則 一個執行緒內,按照程式碼順序,書寫在前面的操作 happens-before 書寫在後面的操作。
  • 鎖定規則 一個unLock操作 happens-before 後面對同一個鎖的lock操作。
  • volatile變數規則 對一個變數的寫操作 happens-before 後面對這個變數的讀操作。
  • 傳遞規則 如果操作A happens-before 操作B,而操作B又 happens-before 操作C,則可以得出操作A happens-before 操作C。
  • 執行緒啟動規則 Thread物件的start()方法 happens-before 此執行緒的每個一個動作。
  • 執行緒中斷規則 對執行緒interrupt()方法的呼叫 happens-before 被中斷執行緒的程式碼檢測到中斷事件的發生。
  • 執行緒終結規則 執行緒中所有的操作都 happens-before 執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行。
  • 物件終結規則 一個物件的初始化完成 happens-before 他的finalize()方法的開始

4.3 記憶體屏障

Java中如何保證底層操作的有序性和可見性?可以通過記憶體屏障。

記憶體屏障是被插入兩個CPU指令之間的一種指令,用來禁止處理器指令發生重排序(像屏障一樣),從而保障有序性的。另外,為了達到屏障的效果,它也會使處理器寫入、讀取值之前,將主記憶體的值寫入快取記憶體,清空無效佇列,從而保障可見性

舉個例子:

Store1; 
Store2;   
Load1;   
StoreLoad;  //記憶體屏障
Store3;   
Load2;   
Load3;
複製程式碼

對於上面的一組CPU指令(Store表示寫入指令,Load表示讀取指令),StoreLoad屏障之前的Store指令無法與StoreLoad屏障之後的Load指令進行交換位置,即重排序。但是StoreLoad屏障之前和之後的指令是可以互換位置的,即Store1可以和Store2互換,Load2可以和Load3互換。

常見有4種屏障

  • LoadLoad屏障: 對於這樣的語句 Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的資料被訪問前,保證Load1要讀取的資料被讀取完畢。
  • StoreStore屏障: 對於這樣的語句 Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
  • LoadStore屏障: 對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被執行前,保證Load1要讀取的資料被讀取完畢。
  • StoreLoad屏障: 對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的(沖刷寫緩衝器,清空無效化佇列)。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種記憶體屏障的功能。

Java中對記憶體屏障的使用在一般的程式碼中不太容易見到,常見的有volatile和synchronized關鍵字修飾的程式碼塊(後面再展開介紹),還可以通過Unsafe這個類來使用記憶體屏障。

4.4 8種操作同步的規則

JMM在執行前面介紹8種基本操作時,為了保證記憶體間資料一致性,JMM中規定需要滿足以下規則:

  • 規則1:如果要把一個變數從主記憶體中複製到工作記憶體,就需要按順序的執行 read 和 load 操作,如果把變數從工作記憶體中同步回主記憶體中,就要按順序的執行 store 和 write 操作。但 Java 記憶體模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。
  • 規則2:不允許 read 和 load、store 和 write 操作之一單獨出現。
  • 規則3:不允許一個執行緒丟棄它的最近 assign 的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體中。
  • 規則4:不允許一個執行緒無原因的(沒有發生過任何 assign 操作)把資料從工作記憶體同步回主記憶體中。
  • 規則5:一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load 或 assign )的變數。即就是對一個變數實施 use 和 store 操作之前,必須先執行過了 load 或 assign 操作。
  • 規則6:一個變數在同一個時刻只允許一條執行緒對其進行 lock 操作,但 lock 操作可以被同一條執行緒重複執行多次,多次執行 lock 後,只有執行相同次數的 unlock 操作,變數才會被解鎖。所以 lock 和 unlock 必須成對出現。
  • 規則7:如果對一個變數執行 lock 操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行 load 或 assign 操作初始化變數的值。
  • 規則8:如果一個變數事先沒有被 lock 操作鎖定,則不允許對它執行 unlock 操作;也不允許去 unlock 一個被其他執行緒鎖定的變數。
  • 規則9:對一個變數執行 unlock 操作之前,必須先把此變數同步到主記憶體中(執行 store 和 write 操作)

看起來這些規則有些繁瑣,其實也不難理解:

  • 規則1、規則2 工作記憶體中的共享變數作為主記憶體的副本,主記憶體變數的值同步到工作記憶體需要read和load一起使用,工作記憶體中的變數的值同步回主記憶體需要store和write一起使用,這2組操作各自都是是一個固定的有序搭配,不允許單獨出現。
  • 規則3、規則4 由於工作記憶體中的共享變數是主記憶體的副本,為保證資料一致性,當工作記憶體中的變數被位元組碼引擎重新賦值,必須同步回主記憶體。如果工作記憶體的變數沒有被更新,不允許無原因同步回主記憶體。
  • 規則5 由於工作記憶體中的共享變數是主記憶體的副本,必須從主記憶體誕生。
  • 規則6、7、8、9 為了併發情況下安全使用變數,執行緒可以基於lock操作獨佔主記憶體中的變數,其他執行緒不允許使用或unlock該變數,直到變數被執行緒unlock。

4.5 volatile型變數的特殊規則

volatile的中文意思是不穩定的,易變的,用volatile修飾變數是為了保證變數的可見性。

volatile的語義

volatile主要有下面2種語義

語義1 保證可見性

保證了不同執行緒對該變數操作的記憶體可見性。

這裡保證可見性是不等同於volatile變數併發操作的安全性,保證可見性具體一點解釋:

執行緒寫volatile變數的過程:

  • 1 改變執行緒工作記憶體中volatile變數副本的值
  • 2 將改變後的副本的值從工作記憶體重新整理到主記憶體

執行緒讀volatile變數的過程:

  • 1 從主記憶體中讀取volatile變數的最新值到執行緒的工作記憶體中
  • 2 從工作記憶體中讀取volatile變數的副本

但是如果多個執行緒同時把更新後的變數值同時重新整理回主記憶體,可能導致得到的值不是預期結果:

舉個例子: 定義volatile int count = 0,2個執行緒同時執行count++操作,每個執行緒都執行500次,最終結果小於1000,原因是每個執行緒執行count++需要以下3個步驟:

  • 步驟1 執行緒從主記憶體讀取最新的count的值
  • 步驟2 執行引擎把count值加1,並賦值給執行緒工作記憶體
  • 步驟3 執行緒工作記憶體把count值儲存到主記憶體 有可能某一時刻2個執行緒在步驟1讀取到的值都是100,執行完步驟2得到的值都是101,最後重新整理了2次101儲存到主記憶體。

語義2 禁止進行指令重排序

具體一點解釋,禁止重排序的規則如下:

  • 當程式執行到 volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
  • 在進行指令優化時,不能將在對 volatile 變數訪問的語句放在其後面執行,也不能把 volatile 變數後面的語句放到其前面執行。

普通的變數僅僅會保證該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證賦值操作的順序與程式程式碼中的執行順序一致。

舉個例子:

volatile boolean initialized = false;

// 下面程式碼執行緒A中執行
// 讀取配置資訊,當讀取完成後將initialized設定為true以通知其他執行緒配置可用
doSomethingReadConfg();
initialized = true;

// 下面程式碼執行緒B中執行
// 等待initialized 為true,代表執行緒A已經把配置資訊初始化完成
while (!initialized) {
     sleep();
}
// 使用執行緒A初始化好的配置資訊
doSomethingWithConfig();
複製程式碼

上面程式碼中如果定義initialized變數時沒有使用volatile修飾,就有可能會由於指令重排序的優化,導致執行緒A中最後一句程式碼 "initialized = true" 在 “doSomethingReadConfg()” 之前被執行,這樣會導致執行緒B中使用配置資訊的程式碼就可能出現錯誤,而volatile關鍵字就禁止重排序的語義可以避免此類情況發生。

volatile型變數實現原理

具體實現方式是在編譯期生成位元組碼時,會在指令序列中增加記憶體屏障來保證,下面是基於保守策略的JMM記憶體屏障插入策略:

volatile型變數記憶體屏障插入策略

  • 在每個volatile寫操作的前面插入一個StoreStore屏障。 該屏障除了保證了屏障之前的寫操作和該屏障之後的寫操作不能重排序,還會保證了volatile寫操作之前,任何的讀寫操作都會先於volatile被提交。

  • 在每個volatile寫操作的後面插入一個StoreLoad屏障。 該屏障除了使volatile寫操作不會與之後的讀操作重排序外,還會重新整理處理器快取,使volatile變數的寫更新對其他執行緒可見。

  • 在每個volatile讀操作的後面插入一個LoadLoad屏障。 該屏障除了使volatile讀操作不會與之前的寫操作發生重排序外,還會重新整理處理器快取,使volatile變數讀取的為最新值。

  • 在每個volatile讀操作的後面插入一個LoadStore屏障。 該屏障除了禁止了volatile讀操作與其之後的任何寫操作進行重排序,還會重新整理處理器快取,使其他執行緒volatile變數的寫更新對volatile讀操作的執行緒可見。

volatile型變數使用場景

總結起來,就是“一次寫入,到處讀取”,某一執行緒負責更新變數,其他執行緒只讀取變數(不更新變數),並根據變數的新值執行相應邏輯。例如狀態標誌位更新,觀察者模型變數值釋出。

4.6 final型變數的特殊規則

我們知道,final成員變數必須在宣告的時候初始化或者在構造器中初始化,否則就會報編譯錯誤。 final關鍵字的可見性是指:被final修飾的欄位在宣告時或者構造器中,一旦初始化完成,那麼在其他執行緒無須同步就能正確看見final欄位的值。這是因為一旦初始化完成,final變數的值立刻回寫到主記憶體。

4.7 synchronized的特殊規則

通過 synchronized關鍵字包住的程式碼區域,對資料的讀寫進行控制:

  • 讀資料 當執行緒進入到該區域讀取變數資訊時,對資料的讀取也不能從工作記憶體讀取,只能從記憶體中讀取,保證讀到的是最新的值。
  • 寫資料 在同步區內對變數的寫入操作,在離開同步區時就將當前執行緒內的資料重新整理到記憶體中,保證更新的資料對其他執行緒的可見性。

4.8 long和double型變數的特殊規則

Java記憶體模型要求lock、unlock、read、load、assign、use、store、write這8種操作都具有原子性,但是對於64位的資料型別(long和double),在模型中特別定義相對寬鬆的規定:允許虛擬機器將沒有被volatile修飾的64位資料的讀寫操作分為2次32位的操作來進行。也就是說虛擬機器可選擇不保證64位資料型別的load、store、read和write這4個操作的原子性。由於這種非原子性,有可能導致其他執行緒讀到同步未完成的“32位的半個變數”的值。

不過實際開發中,Java記憶體模型強烈建議虛擬機器把64位資料的讀寫實現為具有原子性,目前各種平臺下的商用虛擬機器都選擇把64位資料的讀寫操作作為原子操作來對待,因此我們在編寫程式碼時一般不需要把用到的long和double變數專門宣告為volatile。

5 總結

由於Java記憶體模型涉及系列規則,網上的文章大部分就是對這些規則進行解析,但是很多沒有解釋為什麼需要這些規則,這些規則的作用,其實這是不利於初學者學習的,容易繞進去這些繁瑣規則不知所以然,下面談談我的一點學習知識的個人體會:

學習知識的過程不是等同於只是理解知識和記憶知識,而是要對知識解決的問題的輸入和輸出建立連線,知識的本質是解決問題,所以在學習之前要理解問題,理解這個問題要的輸出和輸出,而知識就是輸入到輸出的一個關係對映。知識的學習要結合大量的例子來理解這個對映關係,然後壓縮知識,華羅庚說過:“把一本書讀厚,然後再讀薄”,解釋的就是這個道理,先結合大量的例子理解知識,然後再壓縮知識。

以學習Java記憶體模型為例:

  • 理解問題,明確輸入輸出 首先理解Java記憶體模型是什麼,有什麼用,解決什麼問題
  • 理解記憶體模型系列協議 結合大量例子理解這些協議規則
  • 壓縮知識 大量規則其實就是通過資料同步協議,保證記憶體副本之間的資料一致性,同時防止重排序對程式的影響。

希望對大家有幫助。

(本文同時發表於作者個人部落格 www.jianshu.com/u/ced6b70c7…)

參考

《深入學習Java虛擬機器》

深入拆解Java虛擬機器

Java核心技術36講

Synchronization and the Java Memory Model ——Doug Lea

深入理解 Java 記憶體模型

Java記憶體屏障和可見性

記憶體屏障與synchronized、volatile的原理

阿里雲最近開始發放代金券了,新老使用者均可免費獲取, 新註冊使用者可以獲得1000元代金券,老使用者可以獲得270元代金券,建議大家都領取一份,反正是免費領的,說不定以後需要呢? 阿里雲代金券 領取 promotion.aliyun.com/ntms/yunpar…

熱門活動 高效能雲伺服器特惠 助力企業上雲 效能級主機2-5折 promotion.aliyun.com/ntms/act/en…

相關文章