Java 內存模型

java耕耘 發佈 2020-01-06T12:28:39+00:00

認真寫文章,用心做分享。公眾號:Java耕耘者 文章都會在裡面更新,整理的資料也會放在裡面。

認真寫文章,用心做分享。公眾號:Java耕耘者 文章都會在裡面更新,整理的資料也會放在裡面。

什麼是內存模型

在多CPU的系統中,每個CPU都有多級緩存,一般分為L1、L2、L3緩存,因為這些緩存的存在,提供了數據的訪問性能,也減輕了數據總線上數據傳輸的壓力,同時也帶來了很多新的挑戰,比如兩個CPU同時去操作同一個內存地址,會發生什麼?在什麼條件下,它們可以看到相同的結果?這些都是需要解決的。

所以在CPU的層面,內存模型定義了一個充分必要條件,保證其它CPU的寫入動作對該CPU是可見的,而且該CPU的寫入動作對其它CPU也是可見的,那這種可見性,應該如何實現呢?

有些處理器提供了強內存模型,所有CPU在任何時候都能看到內存中任意位置相同的值,這種完全是硬體提供的支持。

其它處理器,提供了弱內存模型,需要執行一些特殊指令(就是經常看到或者聽到的,memory barriers內存屏障),刷新CPU緩存的數據到內存中,保證這個寫操作能夠被其它CPU可見,或者將CPU緩存的數據設置為無效狀態,保證其它CPU的寫操作對本CPU可見。通常這些內存屏障的行為由底層實現,對於上層語言的程式設計師來說是透明的(不需要太關心具體的內存屏障如何實現)。


前面說到的內存屏障,除了實現CPU之前的數據可見性之外,還有一個重要的職責,可以禁止指令的重排序。

這裡說的重排序可以發生在好幾個地方:編譯器、運行時、JIT等,比如編譯器會覺得把一個變量的寫操作放在最後會更有效率,編譯後,這個指令就在最後了(前提是只要不改變程序的語義,編譯器、執行器就可以這樣自由的隨意優化),一旦編譯器對某個變量的寫操作進行優化(放到最後),那麼在執行之前,另一個線程將不會看到這個執行結果。

當然了,寫入動作可能被移到後面,那也有可能被挪到了前面,這樣的「優化」有什麼影響呢?這種情況下,其它線程可能會在程序實現「發生」之前,看到這個寫入動作(這裡怎麼理解,指令已經執行了,但是在代碼層面還沒執行到)。通過內存屏障的功能,我們可以禁止一些不必要、或者會帶來負面影響的重排序優化,在內存模型的範圍內,實現更高的性能,同時保證程序的正確性。

下面看一個重排序的例子:

Class Reordering {
  int x = 0, y = 0;
  public void writer() {
    x = 1;
    y = 2;
  }

  public void reader() {
    int r1 = y;
    int r2 = x;
  }
}

假設這段代碼有2個線程並發執行,線程A執行writer方法,線程B執行reader方法,線程B看到y的值為2,因為把y設置成2發生在變量x的寫入之後(代碼層面),所以能斷定線程B這時看到的x就是1嗎?

當然不行! 因為在writer方法中,可能發生了重排序,y的寫入動作可能發在x寫入之前,這種情況下,線程B就有可能看到x的值還是0。

在Java內存模型中,描述了在多線程代碼中,哪些行為是正確的、合法的,以及多線程之間如何進行通信,代碼中變量的讀寫行為如何反應到內存、CPU緩存的底層細節。

在Java中包含了幾個關鍵字:volatile、final和synchronized,幫助程式設計師把代碼中的並發需求描述給編譯器。Java內存模型中定義了它們的行為,確保正確同步的Java代碼在所有的處理器架構上都能正確執行。

synchronization 可以實現什麼

Synchronization有多種語義,其中最容易理解的是互斥,對於一個monitor對象,只能夠被一個線程持有,意味著一旦有線程進入了同步代碼塊,那麼其它線程就不能進入直到第一個進入的線程退出代碼塊(這因為都能理解)。

但是更多的時候,使用synchronization並非單單互斥功能,Synchronization保證了線程在同步塊之前或者期間寫入動作,對於後續進入該代碼塊的線程是可見的(又是可見性,不過這裡需要注意是對同一個monitor對象而言)。在一個線程退出同步塊時,線程釋放monitor對象,它的作用是把CPU緩存數據(本地緩存數據)刷新到主內存中,從而實現該線程的行為可以被其它線程看到。在其它線程進入到該代碼塊時,需要獲得monitor對象,它在作用是使CPU緩存失效,從而使變量從主內存中重新加載,然後就可以看到之前線程對該變量的修改。

但從緩存的角度看,似乎這個問題只會影響多處理器的機器,對於單核來說沒什麼問題,但是別忘了,它還有一個語義是禁止指令的重排序,對於編譯器來說,同步塊中的代碼不會移動到獲取和釋放monitor外面。

下面這種代碼,千萬不要寫,會讓人笑掉大牙:

synchronized (new Object()) {}

這實際上是沒有操作的操作,編譯器完成可以刪除這個同步語義,因為編譯知道沒有其它線程會在同一個monitor對象上同步。

所以,請注意:對於兩個線程來說,在相同的monitor對象上同步是很重要的,以便正確的設置happens-before關係。

final 可以影響什麼

如果一個類包含final欄位,且在構造函數中初始化,那么正確的構造一個對象後,final欄位被設置後對於其它線程是可見的。

這裡所說的正確構造對象,意思是在對象的構造過程中,不允許對該對象進行引用,不然的話,可能存在其它線程在對象還沒構造完成時就對該對象進行訪問,造成不必要的麻煩。

class FinalFieldExample {
  final int x;
  int y;
  static FinalFieldExample f;
  public FinalFieldExample() {
    x = 3;
    y = 4;
  }

  static void writer() {
    f = new FinalFieldExample();
  }

  static void reader() {
    if (f != null) {
      int i = f.x;
      int j = f.y;
    }
  }
}

上面這個例子描述了應該如何使用final欄位,一個線程A執行reader方法,如果f已經在線程B初始化好,那麼可以確保線程A看到x值是3,因為它是final修飾的,而不能確保看到y的值是4。 如果構造函數是下面這樣的:

public FinalFieldExample() { // bad!
  x = 3;
  y = 4;
  // bad construction - allowing this to escape
  global.obj = this;
}

這樣通過global.obj拿到對象後,並不能保證x的值是3.

###volatile可以做什麼 Volatile欄位主要用於線程之間進行通信,volatile欄位的每次讀行為都能看到其它線程最後一次對該欄位的寫行為,通過它就可以避免拿到緩存中陳舊數據。它們必須保證在被寫入之後,會被刷新到主內存中,這樣就可以立即對其它線程可以見。類似的,在讀取volatile欄位之前,緩存必須是無效的,以保證每次拿到的都是主內存的值,都是最新的值。volatile的內存語義和sychronize獲取和釋放monitor的實現目的是差不多的。

對於重新排序,volatile也有額外的限制。

下面看一個例子:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}

同樣的,假設一個線程A執行writer,另一個線程B執行reader,writer中對變量v的寫入把x的寫入也刷新到主內存中。reader方法中會從主內存重新獲取v的值,所以如果線程B看到v的值為true,就能保證拿到的x是42.(因為把x設置成42發生在把v設置成true之前,volatile禁止這兩個寫入行為的重排序)。

如果變量v不是volatile,那麼以上的描述就不成立了,因為執行順序可能是v=true, x=42,或者對於線程B來說,根本看不到v被設置成了true。

JVM內存操作的並發問題

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

  • 1 工作內存數據一致性 各個線程操作數據時會保存使用到的主內存中的共享變量副本,當多個線程的運算任務都涉及同一個共享變量時,將導致各自的的共享變量副本不一致,如果真的發生這種情況,數據同步回主內存以誰的副本數據為準? Java內存模型主要通過一系列的數據同步協議、規則來保證數據的一致性,後面再詳細介紹。
  • 2 指令重排序優化 Java中重排序通常是編譯器或運行時環境為了優化程序性能而採取的對指令進行重新排序執行的一種手段。重排序分為兩類:編譯期重排序和運行期重排序,分別對應編譯時和運行時環境。 同樣的,指令重排序不是隨意重排序,它需要滿足以下兩個條件: 1 在單線程環境下不能改變程序運行的結果 即時編譯器(和處理器)需要保證程序能夠遵守 as-if-serial 屬性。通俗地說,就是在單線程情況下,要給程序一個順序執行的假象。即經過重排序的執行結果要與順序執行的結果保持一致。 2 存在數據依賴關係的不允許重排序

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

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種基本操作


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

Java內存模型運行規則

內存交互基本操作的3個特性

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

  • 原子性(Atomicity) 即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。即使在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程所干擾。
  • 可見性(Visibility) 是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。 正如上面「交互操作流程」中所說明的一樣,JMM是通過在線程1變量工作內存修改後將新值同步回主內存,線程2在變量讀取前從主內存刷新變量值,這種依賴主內存作為傳遞媒介的方式來實現可見性。
  • 有序性(Ordering) 有序性規則表現在以下兩種場景: 線程內和線程間 線程內 從某個線程的角度看方法的執行,指令會按照一種叫「串行」(as-if-serial)的方式執行,此種方式已經應用於順序程式語言。 線程間 這個線程「觀察」到其他線程並發地執行非同步的代碼時,由於指令重排序優化,任何代碼都有可能交叉執行。唯一起作用的約束是:對於同步方法,同步塊(synchronized關鍵字修飾)以及volatile欄位的操作仍維持相對有序。

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

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()方法的開始

內存屏障

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這個類來使用內存屏障。

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。

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寫操作的前面插入一個StoreStore屏障。 該屏障除了保證了屏障之前的寫操作和該屏障之後的寫操作不能重排序,還會保證了volatile寫操作之前,任何的讀寫操作都會先於volatile被提交。
  • 在每個volatile寫操作的後面插入一個StoreLoad屏障。 該屏障除了使volatile寫操作不會與之後的讀操作重排序外,還會刷新處理器緩存,使volatile變量的寫更新對其他線程可見。
  • 在每個volatile讀操作的後面插入一個LoadLoad屏障。 該屏障除了使volatile讀操作不會與之前的寫操作發生重排序外,還會刷新處理器緩存,使volatile變量讀取的為最新值。
  • 在每個volatile讀操作的後面插入一個LoadStore屏障。 該屏障除了禁止了volatile讀操作與其之後的任何寫操作進行重排序,還會刷新處理器緩存,使其他線程volatile變量的寫更新對volatile讀操作的線程可見。

volatile型變量使用場景

總結起來,就是「一次寫入,到處讀取」,某一線程負責更新變量,其他線程只讀取變量(不更新變量),並根據變量的新值執行相應邏輯。例如狀態標誌位更新,觀察者模型變量值發布。

final型變量的特殊規則

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

synchronized的特殊規則

通過 synchronized關鍵字包住的代碼區域,對數據的讀寫進行控制:

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

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。

總結

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

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

以學習Java內存模型為例:

  • 理解問題,明確輸入輸出 首先理解Java內存模型是什麼,有什麼用,解決什麼問題
  • 理解內存模型系列協議 結合大量例子理解這些協議規則
  • 壓縮知識 大量規則其實就是通過數據同步協議,保證內存副本之間的數據一致性,同時防止重排序對程序的影響。
認真寫文章,用心做分享。公眾號:Java耕耘者   文章都會在裡面更新,整理的資料也會放在裡面。
關鍵字: