Java內存模型(JMM):一個可能知道但沒具體瞭解的概念

寫個文章是由於一次字節面試中,問到java內存模型瞭解嗎?我答了一些堆、方法區、虛擬機棧什麼的。而後說這個不是。我一臉矇蔽。。。以後瞭解到JMM,才知道本身有多蠢,原來是這些東西,原來這些叫JMM。java

因此,如今寫一篇文章總結一下。面試

1. Java內存模型的概念

你們都知道java是經過java虛擬機來跨平臺運行。但,它是怎麼實現的呢,有沒有什麼規則?數組

:不一樣計算機操做系統對內存模型操做不同,這時候就要有統一的規範來完成操做。因此就要經過JAVA內存模型(Java Memory Model,JMM緩存

  • 它是一種JAVA虛擬機規範
  • 它屏蔽各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。

!!!下面2,3,4,5,6段落都是有關JAVA內存模型的相關規範或者規則。!!!安全

文章最後作總結多線程

2. 內存規範:主內存和工做內存

Java內存模型的主要目的是定義程序中各類變量的訪問規則併發

  • 關注在虛擬機中把變量值存儲到內存從內存中取出變量值這樣的底層細節。

這個段落的目標:針對的是線程之間能夠共享的變量app

變量根據是否能夠共享劃分爲:線程私有的和線程公有的。函數

  • 線程私有:局部變量與方法參數
  • 線程公有:實例字段、靜態字段和構成數組對象的元素

Java內存模型規定了全部的變量都存儲在主內存中。每條線程還有本身的工做內存,線程的工做內存中保存了被該線程使用的變量的主內存副本,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存中的數據。如圖所示:性能

這一部分要和JAVA內存區域做區分。

  • 內存區域分爲:虛擬機棧、本地方法棧、程序計數器(加粗爲線程之間隔離的)、方法區、堆區、直接內存

3. 內存操做

主內存與工做內存之間具體的交互協議,

  • 即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存這一類的實現細節。

Java內存模型中定義瞭如下8種操做來完成。Java虛擬機實現時必須保證下面說起的每一種操做都是原子的、不可再分的。(簡單看看就行

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

Java設計團隊,將Java內存模型的操做簡化爲read、write、lock和unlock四種,但這只是語言描述上的等價化簡,Java內存模型的基礎設計並未改變。

4. volatile型變量的規則

volatile定義的變量有兩個特性:

  • 保證此變量對全部線程的可見性
  • 禁止指令重排序優化

4.1 可見性

1. 概念:當一條線程修改了這個變量的值,新值對於其餘線程來講是能夠當即得知的。而普通變量並不能作到這一點,普通變量的值在線程間傳遞時均須要經過主內存來完成

  • 普通變量寫入讀取流程:線程A修改一個普通變量的值,而後向主內存進行回寫,另一條線程B在線程A回寫完成了以後再對主內存進行讀取操做,新變量值纔會對線程B可見。

2. 線程安全

在併發中,並不必定是線程安全。

Java裏面的運算操做(這裏指的是a=b+1,相似這種,不是a=1這樣)符並不是原子操做,這致使volatile變量的運算在併發下同樣是不安全的。

網上有不少利用線程對一個變量10000次,可是最後結果不是10000*線程數

變量的++操做在字節碼中分解爲三個部分(此處並不嚴謹,表明意思爲分紅多步驟),這樣會致使線程不安全(單獨的讀寫是安全的)。

  • 讀值
  • 改值
  • 存值

4.2 禁止指令重排序

1. 概念:是指處理器採用了容許將多條指令不按程序規定的順序分開發送給各個相應的電路單元進行處理。但並非說指令任意重排,處理器必須能正確處理指令依賴狀況保障程序能得出正確的執行結果。

注意:在同一個線程的方法執行過程當中沒法感知到指令重排序,可是其實其中的一些執行順序發生了改變但保證結果不變

  • 由於Java內存模型中定義「線程內表現爲串行的語義」。

2. 禁止指令重排序的例子:單例模式懶漢

class Singleton{
  private static volatile Singleton instance = null;
  private Singleton(){}
  public static Singleton getInstance(){
    if(instance == null){
      synchronized(Singleton.class){
        if(instance == null){
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}
複製代碼

代碼解釋:假如沒有volatile修飾,在new Singleton的時候,對instance已經賦予了內存空間,可是內存中沒有東西。此時有另外一個線程獲取單例去使用,發現這個內存中沒有對象沒法使用(就是初始化一半),發生了線程安全的問題。

原理解釋:彙編指令中增長lock修飾

  • 它將本處理器的緩存寫入了內存,該寫入動做也會引發別的處理器或者別的內核無效化其緩存。
  • 它至關於一個內存屏障(Memory Barrier或Memory Fence,指重排序時不能把後面的指令重排序到內存屏障以前的位置,注意不要與第3章中介紹的垃圾收集器用於捕獲變量訪問的內存屏障互相混淆)

3. 使用原則

  • volatile變量讀操做的性能消耗與普通變量幾乎沒有什麼差異,可是寫操做則可能會慢上一些,由於它須要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。不過即使如此,大多數場景下volatile的總開銷仍然要比鎖來得更低
  • 咱們在volatile與鎖中選擇的惟一判斷依據僅僅是volatile的語義可否知足使用場景的需求。

4.3 總結

  1. 要求在工做內存中,每次使用V前都必須先從主內存刷新最新的值,用於保證能看見其餘線程對變量V所作的修改。
  2. 要求在工做內存中,每次修改V後都必須馬上同步回主內存中,用於保證其餘線程能夠看到本身對變量V所作的修改。
  3. 要求volatile修飾的變量不會被指令重排序優化,從而保證代碼的執行順序與程序的順序相同

5. long和double的特殊規則

對於上面的Java內存模型要求lock、unlock、read、load、assign、use、store、write這八種操做都具備原子性。

可是對於64位的數據類型(long和double),在模型中特別定義了一條寬鬆的規定:容許虛擬機將沒有被volatile修飾的64位數據的讀寫操做劃分爲兩次32位的操做來進行,

  • 容許虛擬機實現自行選擇是否要保證64位數據類型的load、store、read和write這四個操做的原子性,這就是所謂的「long和double的非原子性協定」

通過實際測試,在目前主流平臺下商用的64位Java虛擬機中並不會出現非原子性訪問行爲,可是對於32位的Java虛擬機,譬如比較經常使用的32位x86平臺下的HotSpot虛擬機,對long類型的數據確實存在非原子性訪問的風險。

  • 編寫代碼時通常不須要由於這個緣由刻意把用到的long和double變量專門聲明爲volatile。

6. happens-before原則

若是Java內存模型中全部的有序性都僅靠volatile和synchronized來完成,那麼有不少操做都將會變得很是囉嗦,可是咱們在編寫Java併發代碼的時候並無察覺到這一點,這是由於Java語言中有一個「先行發生」(Happens-Before)的原則。

Java內存模型下一些自然的先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,能夠在編碼中直接使用。存在8中規則:

  1. 程序次序規則(Program Order Rule):在一個線程內,按照控制流順序,書寫在前面的操做先行發生於書寫在後面的操做。注意,這裏說的是控制流順序而不是程序代碼順序,由於要考慮分支、循環等結構。
  2. 管程鎖定規則(Monitor Lock Rule):一個unlock操做先行發生於後面對同一個鎖的lock操做。這裏必須強調的是「同一個鎖」,而「後面」是指時間上的前後。
  3. volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做,這裏的「後面」一樣是指時間上的前後。
  4. 線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每個動做。
  5. 線程終止規則(Thread Termination Rule):線程中的全部操做都先行發生於對此線程的終止檢測,咱們能夠經過Thread::join()方法是否結束、Thread::isAlive()的返回值等手段檢測線程是否已經終止執行。
  6. 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread::interrupted()方法檢測到是否有中斷髮生。
  7. 對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。
  8. 傳遞性(Transitivity):若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。

7.總結

7.1 原子性

由Java內存模型來直接保證的原子性變量操做包括read、load、assign、use、store和write這六個,咱們大體能夠認爲,基本數據類型的訪問、讀寫都是具有原子性的。

  • 例外就是long和double的非原子性協定,讀者只要知道這件事情就能夠了,無須太過在乎這些幾乎不會發生的例外狀況。

若是應用場景須要一個更大範圍的原子性保證,Java內存模型還提供synchronized關鍵字,所以在synchronized塊之間的操做也具有原子性。

  • 經過lock和unlock操做,但虛擬機未把lock和unlock操做直接開放給用戶使用,可是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱式地使用這兩個操做。

7.2 可見性

可見性就是指當一個線程修改了共享變量的值時,其餘線程可以當即得知這個修改。

  • volatile的可見性是,volatile的特殊規則保證了新值能當即同步到主內存,以及每次使用前當即從主內存刷新。所以咱們能夠說volatile保證了多線程操做時變量的可見性,而普通變量則不能保證這一點。
  • synchronized同步塊的可見性是由「對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store、write操做)」這條規則得到的。
  • final關鍵字的可見性是指:被final修飾的字段在構造器中一旦被初始化完成,而且構造器沒有把「this」的引用傳遞出去(this引用逃逸是一件很危險的事情,其餘線程有可能經過這個引用訪問到「初始化了一半」的對象),那麼在其餘線程中就能看見final字段的值。

7.3 有序性

Java程序中自然的有序性能夠總結爲:

  • 若是在本線程內觀察,全部的操做都是有序的;
  • 若是在一個線程中觀察另外一個線程,全部的操做都是無序的。

前半句是指線程內似表現爲串行的語義,後半句是指指令重排序現象和工做內存與主內存同步延遲現象。

  • volatile關鍵字自己就包含了禁止指令重排序的語義。
  • synchronized則是由一個變量在同一個時刻只容許一條線程對其進行lock操做這條規則得到的,這個規則決定了持有同一個鎖的兩個同步塊只能串行地進入

這篇文章參考:《深刻理解Java虛擬機(第3版)》

相關文章
相關標籤/搜索