1. 程式人生 > >(八)java併發程式設計--java記憶體模型

(八)java併發程式設計--java記憶體模型

首先是什麼是java記憶體模型?

  不同的作業系統有不同的記憶體模型,“記憶體模型”一詞可以理解為在特定操作寫一下,對特定的記憶體或者快取記憶體進行讀寫訪問的抽象過程。
  不同的物理機有不同的記憶體模型。而java記憶體模型是來遮蔽掉各種不同物理機及其不同作業系統的記憶體訪問差異,以實現java程式在各種平臺下都能達到一致的訪問效果。
  java 記憶體模型解釋了java虛擬機器是如何與計算機記憶體(RAM)工作的。
  下面先了解一下計算機硬體的記憶體模型。

計算機硬體記憶體模型

  為了更充分的利用計算機處理器的效能,當處理器需要與記憶體互動,需要讀取運算資料、儲存運算結果等,這些IO操作是很難消除的。
  由於計算機的儲存裝置和處理器的運算速度有幾個數量級的差距,所以現代計算機系統不得不加入一層讀寫速度儘可能接近處理器運算速度的快取記憶體來作為記憶體與處理器之間的緩衝:將運算需要使用的資料複製到快取中,讓運算可以快速進行,當運算結束後再從快取同步回記憶體之中,這樣處理器就無須等待緩慢的記憶體讀寫了。如下圖是計算機硬體記憶體模型。
計算機記憶體模型


  現代計算機一般是2個或者更多cpu,或者一個cpu有多核,這樣就可以同時執行多個執行緒。

  java記憶體模型和計算機硬體記憶體模型類似,java虛擬機器也有自己的記憶體模型。

  處理器可能會對輸入的程式碼亂碼執行優化,處理器會對亂序執行的結果重組,保證該結果與順序執行的結果是一致的。但並不保證程式中各個語句計算的先後順序與輸入程式碼中的順序一致,因此如果在一個計算機任務依賴另外一個計算任務中間結果,那麼其順序性不能靠程式碼先後順序來保證。類似的,java虛擬機器即時編譯器也有類似指令重排序的優化。

java記憶體模型

  jdk1.5後java記憶體模型逐漸完善。
  java記憶體模型主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數的底層細節。
  這裡的變數不包括區域性變數與方法引數,他們為執行緒私有。這裡的記憶體模型中的變數包括例項欄位、靜態欄位和構成陣列物件的元素,JMM對他們的管理。
java記憶體模型規定了所有變數(例項欄位、靜態欄位和構成陣列物件的元素),都在自己的主記憶體中。每個執行緒都有自己的工作記憶體,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀取主記憶體中的變數。執行緒工作空間儲存了該執行緒使用的變數的副本拷貝。(ps:這裡的主存是jvm虛擬機器主存,是計算機主存的一部分)執行緒間變數之間的傳遞需要通過主記憶體來完成。
如下圖所示

java記憶體模型

  這與之前記憶體結構有什麼連線呢?可以說是從兩個方面對jvm記憶體不同層次的劃分,java記憶體模型是從”邏輯角度”出發,而把jvm記憶體劃分為
  堆和棧等是從結構方面的劃分,我認為的聯絡如下下圖。看問題的橫向和縱向。
不同層次的記憶體劃分

java虛擬機器執行時資料區

  執行緒隔離資料區(每個執行緒私有)的部分包括,虛擬機器棧、本地方法棧、程式計數器。
  所有執行緒共享的部分包括方法區和堆。其中

1、程式計數器

  程式計數器,是一塊較小的空間,它可以看做當前執行緒所執行的位元組碼的行號指示器,位元組碼直譯器工作時就是通過改變計數器的值來選取下一條需要執行的位元組碼指令。
  每個執行緒有獨立的計數器,讓各個執行緒互不影響,獨立儲存,是“執行緒私有記憶體”。java虛擬機器規範中是唯一一個沒有規定任何OutOfMemoryError情況的區域。

2、 虛擬機器棧

  和程式計數器一樣,是執行緒私有的,它的生命週期與執行緒相同。
  虛擬機器棧是描述java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用於儲存區域性變數、運算元棧、動態連結、方法出口等資訊。每個方法從呼叫直至完成的過程,就對應著一個棧幀在虛擬機器中入棧到出棧的過程。
  區域性變數存放的是各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用型別。

  這個區域規定了兩種異常情況:1 如果執行緒請求棧的深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常。2 如果是虛擬機器棧擴充套件時無法申請到足夠空間時,會丟擲
OutOfMemoryError異常。

3、 本地方法棧

  與虛擬機器棧類似,不同的是是為虛擬機器使用的Native方法服務。與虛擬機器棧一樣,也會丟擲StackOverflowError和OutOfMemory異常。

執行緒共享資料區,對和方法區

4、 堆

  java堆是java虛擬機器記憶體中最大的一塊。java對是被所有執行緒共享的一塊區域,線上程啟動時建立,這個區的唯一目的就是存放物件例項以及陣列都會在堆上分配。
  當前虛擬機器都是按照可擴充套件實現的(通過-Xmx和-Xms)如果在堆中沒有完成例項分配,並且堆也無法擴充套件時,將會丟擲OutOfMemoryError異常。

5、 方法區

  雖然叫方法區,但是存放的是類的資訊,常量,靜態變數、即時編譯器編譯後的程式碼等資料。
  與java堆一樣各個執行緒共享的記憶體區域。
  當方法區無法滿足記憶體需要時,將丟擲OutOfMemoryError異常。

  可以把執行緒共享區java虛擬機器棧、計數器空間以及本地方法棧統一說成執行緒的棧(執行緒的私有空間),把方法區和堆(執行緒共享區)表示為堆,java記憶體的邏輯檢視可以表示如下圖:
JMM

執行緒非共享區
  JVM每個執行緒都有自己的執行緒棧,執行緒棧包含了當前執行緒執行的方法呼叫相關資訊,我們也把它稱為呼叫棧。隨著程式碼的變化,呼叫棧會不斷變化。

  執行緒棧還包含了當前方法的所有區域性變數,一個執行緒只能讀取自己的執行緒棧,也就是說,執行緒中的本地變數對其他執行緒是不可見的。
  即使兩個執行緒執行的是同一段程式碼,他們也會各自在自己的執行緒棧中建立本地變數,因此每個執行緒有自己的本地變數版本。

  所有的基礎型別(boolean、byte、short、char、int、long、float、double)的變數都將直接儲存線上程棧中,對於它們的值各個執行緒之間是相互獨立的。
  基礎型別的區域性變數,一個執行緒可以傳遞一個副本給另一個執行緒,但是它們之間是無法共享的。

  區域性變數無論是原始型別還是物件的引用,都會放到棧中。(物件本身在堆中)

  物件的成員方法,方法中含有區域性變數,則需要儲存在棧區,即使他們所屬的物件在堆中。

執行緒共享區
  堆中包含了java應用建立物件的所有物件資訊,無論是哪個執行緒建立的,都會放到堆中。也包括原始型別的封裝,也就是包裝型別。
  不管物件屬於一個成員變數還是方法中的本地變數,都會被儲存在堆中。
變數的儲存

  對於一個物件的成員變數,不管是原始型別還是包裝型別,都會被儲存在堆中。
  static型別變數以及類本身相關資訊都會隨著類本身儲存在堆中。

  堆中的堆物件可以被多個執行緒共享。如果一個執行緒獲得物件的應用,它便可以訪問物件的成員變數。如果兩個執行緒同時呼叫了物件的同一個方法,
  那麼兩個執行緒便可以同時方法物件的成員變數,但是對於區域性變數,每個執行緒則會拷貝一份到自己的執行緒棧中。下面的圖展示瞭如下描述:

類中的成員變數和方法中的變數

記憶體

java記憶體模型與計算機記憶體之間的連線

  計算機硬體記憶體架構和java記憶體模型關係,如下圖:

JMM與計算機記憶體

當物件和變數儲存到計算機各個記憶體區域時,必然會面臨一些問題。

原子性、可見性與有序性

  java記憶體模型圍繞著併發過程中如何處理原子性、可見性和有序性這三個特徵來簡歷的,分別如下。

1.原子性

  原子性(Atomicity):由JMM直接保證原子性變數操作包括讀、載入、賦值、使用、儲存和寫入,基本資料型別的讀寫是具有原子性的。
更大範圍的原子,JMM提供了lock和unlock操作來滿足需求,儘管JVM沒有把lock和unlock操作直接開放給我們,但是提供了更高層的指令monitorenter和
monitorexit,反應在java程式碼中也就是synchronized關鍵字,synchronized塊之間的操作具有原子性。

2.可見性

  可見性是指一個執行緒修改了共享變數的值,其他執行緒能立即知道這個修改。JMM是通過每個執行緒自己空間中擁有變數副本,修改後將新值同步回主記憶體,
變數的讀取依賴於主存重新整理變數,普通變數和volatile變數都是如此,volatile變數與普通變數區別是,volatile保證了新值可以立即同步到主記憶體中,以及每次
使用前立即從主記憶體重新整理,這種可見性,普通變數不能保證。

  除了volatile外,java還有兩個關鍵字synchronized和final也可以實現可見性。synchronized塊的可見性是“對於一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中”,
而final關鍵字可見性是指,被final修飾的欄位構造器一旦初始化完成,並且構造器並沒有把“this”引用傳遞出去,那在其他執行緒中就能看到final欄位的值。

3.有序性

  java程式的有序性總結為,如果在本執行緒內觀察,所有的操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有操作都是無序的。
前半句是“執行緒內表現的序列語句”,後半句“指令重排”現象和“工作記憶體與主記憶體同步延遲”現象。
  java volatile和synchronized保證了執行緒的有序性,禁止指令重排列語義,而synchronized決定了持有同一個鎖的兩個同步塊只能序列輸入。

指令重排*

在執行程式時,為了提高效能,編譯器和處理器會對指令做重排序。但是,JMM確保在不同的編譯器和不同的處理器平臺之上,通過插入特定型別的Memory
Barrier(柵欄)來禁止特定型別的編譯器重排序和處理器重排序,為上層提供一直的記憶體可見性保證。

   1、編譯器優化重排序:編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。
  2、指令級並行的重排序:如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
  3、記憶體系統的重排序:處理器使用快取和讀寫快取,這使得載入和儲存操作上看上去可能是亂序執行。

資料依賴性
  如果兩個操作訪問同一個變數,其中一個為寫操作,此時這兩個操作之間存在資料依賴性。
  編譯器和處理器不會改變存在資料依賴性關係的兩個操作的執行順序,即不會重排序。
as-if-serial
  不管怎麼重排序,單執行緒下的執行結果不能被改變,編譯器、rentime和處理器都必須遵守as-if-serial。

Happen-before原則 (先行發生原則)*

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

總結