Java多執行緒-帶你認識Java記憶體模型,記憶體分割槽,從原理剖析Volatile關鍵字

那個人發表於2019-03-04

寫在前面(語句修改版)

讀完本篇文章你將知道:

  • Java的記憶體模型。

  • Java的記憶體分割槽。

  • 全域性變數、區域性變數、物件、例項再記憶體中的位置。

  • JVM重排序機制。

  • JVM的原子性、可見性、有序性。

  • 徹底瞭解Volatile關鍵字。

一. Java的記憶體模型

Java記憶體模型即Java Memory Model,簡稱JMM。JMM定義了Java 虛擬機器(JVM)在計算機記憶體(RAM)中的工作方式。JVM是整個計算機虛擬模型,所以JMM是隸屬於JVM的。想要掌握Java並非執行緒JMM一定要了解。Java記憶體模型定義了多執行緒之間共享變數的可見性以及如何在需要的時候對共享變數進行同步。這裡涉及到共享記憶體區域的知識,稍後會在Java的記憶體分割槽中介紹到。簡單來說JMM解釋了這麼一個問題:當多個執行緒再訪問同一個變數的時候,其中一個執行緒改變了該變數的值但是並未寫入主存中,那麼其他執行緒就會讀取到舊值,無法獲取到最新的值。好了接下來看看什麼是記憶體模型:

Java記憶體模型定義了執行緒和主存(可以理解為java記憶體分割槽中的共享區域,稍後將介紹)之間的抽象關係:執行緒之間的共享變數存貯在主存中,每個執行緒都會擁有屬於自己的私有工作記憶體(這個記憶體分配再棧裡面),再工作記憶體中只會儲存該執行緒使用到的共享變數的副本。這裡的私有工作記憶體其實是一個抽象的概念,它包括了快取、寫緩衝區、暫存器等區域。Java記憶體模型控制執行緒間的通訊,它決定一個執行緒對主存共享變數的寫入何時對另一個執行緒可見。這是Java記憶體模型抽象圖:

從圖中我們能分析出:

1.每個執行緒再執行的時候都會有自己的工作記憶體,其中包括了方法裡面所包含的所有變數等。

2.每個執行緒的私有工作記憶體是不能相互訪問的,這也就解釋了為什麼我們不能再一個方法中訪問另一個方法的區域性變數。

3.當執行緒想要訪問共享變數的時候,需要從主存中獲取,再自己的方法區中只是儲存的變數的副本。

4.當我們修改完共享變數的時候,需要把改過的變數寫入主存中,這樣才能讓其他執行緒獲取到正確的值。

簡單一點就是:

(1)執行緒A把執行緒A本地記憶體中更新過的共享變數重新整理到貯存中去。

(2)執行緒B到主存中去讀取執行緒A之前已更新過的共享變數的的值。

也就是:


int i= 1;複製程式碼

也就是說,這句程式碼被執行緒執行的時候是這樣的情景:執行執行緒先把變數i的值的一個副本,存放到自己的工作記憶體中,然後再把值寫入主存中,而不是直接寫入到主存中。

這樣是不是就可以說明用一個普通的變數作為標記去打斷執行緒是不嚴謹的,大家可以移步到我的上一篇文章如何正確的打斷執行緒

二. Java的記憶體分割槽

一般來說,Java程式在執行時會涉及到以下記憶體區域:

  1. 暫存器:

JVM內部虛擬暫存器,存取速度非常快,程式不可控制。

  1. Java虛擬機器棧(通俗就是我們常說的“棧”):

它是執行緒私有的,它的生命週期與執行緒相同。每個方法被執行的時候都會同時建立一個棧幀(StackFrame)用於儲存區域性變數表、操作棧、動態連結、方法出口等資訊。每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。它存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用(reference型別),它不等同於物件本身,根據不同的虛擬機器實現,它可能是一個指向物件起始地址的引用指標,也可能指向一個代表物件的控制程式碼或者其他與此物件相關的位置)和returnAddress型別(指向了一條位元組碼指令的地址)。

  1. 堆:

Java堆(Java Heap)是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。這一點在Java虛擬機器規範中的描述是:所有的物件例項以及陣列都要在堆上分配。 Java堆是垃圾收集器管理的主要區域。

  1. 方法區:

方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來,但它還是屬於堆裡面的。

  1. 常量池(其實是方法區的一部分):

JVM為每個已載入的型別維護一個常量池,常量池就是這個型別用到的常量的一個有序集合。包括直接常量(基本型別,String)和對其他型別、方法、欄位的符號引用(1)。池中的資料和陣列一樣通過索引訪問。由於常量池包含了一個型別所有的對其他型別、方法、欄位的符號引用,所以常量池在Java的動態連結中起了核心作用。常量池存在於堆中.

需要注意的一些:

  1. 物件所擁有的方法以及裡面涉及到的變數都儲存在棧裡面,方法裡面使用到的全域性變數是隨著物件例項一起儲存在堆裡面,在方法中使用的時候也是使用該全域性變數的副本.

  2. 對於一個物件的成員變數,不管他是原始型別還是包裝型別,都會被存貯在堆區.

  3. 方法區和堆是一樣,是各個執行緒共享的區域,裡面存放java虛擬機器載入的類資訊,常量,靜態變數,即使編譯器編譯後的程式碼等資料.

  4. 當呼叫一個物件的方法時會在java(虛擬機器棧)棧裡面建立屬於自己的棧空間,方法走完即被釋放

  5. 分清什麼是例項什麼是物件。Class a = new Class();此時a叫例項,而不能說a是物件。例項在棧中,物件在堆中,操作例項實際上是通過例項的指標間接操作物件。多個例項可以指向同一個物件。

那麼我們通過程式碼來進一步的認識每個分割槽:


public class Persion{

privite String name = “Wang”;

privite static String love = “eat”;

public void init(int age){

if(age < 0){

age = 0;

}

Log.e(TAG,"Name is "+ name+"Age is "+ age);

}

}複製程式碼

首先我們知道 當我用 Persion p = new Perison()的時候,Persion p 這個引用存貯再棧裡面,new Perison()這個物件儲存在堆裡面,包括name成員變數都在堆裡面;love這個靜態變數存貯在常量池裡面。當我們呼叫 p.init(10) 的時候,會在該執行緒所在的棧裡面開創該執行緒私有的棧記憶體,用來儲存age變數和name共享變數的副本。這裡要說一下,堆、方法區被稱為共享區域,這裡面的資料才能被多執行緒所共享。

三. JVM重排序機制

在虛擬機器層面,為了儘可能減少記憶體操作速度遠慢於CPU執行速度所帶來的CPU空置的影響,虛擬機器會按照自己的一些規則(這規則後面再敘述)將程式編寫順序打亂——即寫在後面的程式碼在時間順序上可能會先執行,而寫在前面的程式碼會後執行——以儘可能充分地利用CPU。拿上面的例子來說:假如不是a=1的操作,而是a=new byte[1024*1024],那麼它會執行地很慢,此時CPU是等待其執行結束呢,還是先執行下面那句flag=true呢?顯然,先執行flag=true可以提前使用CPU,加快整體效率,當然這樣的前提是不會產生錯誤(什麼樣的錯誤後面再說)。雖然這裡有兩種情況:後面的程式碼先於前面的程式碼開始執行;前面的程式碼先開始執行,但當效率較慢的時候,後面的程式碼開始執行並先於前面的程式碼執行結束。不管誰先開始,總之後面的程式碼在一些情況下存在先結束的可能。我們看下簡單的例子:


public void execute(){

int a=0;

int b=1;

int c=a+b;

}複製程式碼

這裡a=0,b=1兩句可以隨便排序,不影響程式邏輯結果。所以程式再執行的時候會選擇先執行int b = 1 ;然後再執行 int a=0;但是我們是無法觀察到的,這確是可能發生的,這句c=a+b這句必須在前兩句的後面執行,所以在他的前後不會出現重排序。這裡我們就簡單的瞭解下就可以啦.

四. JVM的原子性、可見性、有序性

  • 原子性

定義:對基本型別變數的讀取和賦值操作是原子性操作,即這些操作是不可中斷的,要麼執行完畢,要麼就不執行。


x =3;    //語句1

y =4    //語句2

z = x+y ;//語句3

x++;    //語句4複製程式碼

這裡面的操作只有語句1和語句2是原子性的操作,語句3,4不是原子性的操作;因為在語句3中包括了三個操作,1是先讀取x的值,2讀取y的值,3將z的值寫入記憶體中。語句4的解釋是一樣的。一般的一個語句含有多個操作該語句就不是原子性的操作,只有簡單的讀取和賦值才是原子性的操作。

  • 可見性

就是指執行緒之間的可見性,一個執行緒修改的狀態對另一個執行緒是可見的。也就是一個執行緒修改結果,另一個執行緒馬上就能看到。

  • 有序性

Java記憶體模型允許編譯器和處理器對指令進行重排序,雖然重排序不會影響到單執行緒的正確性,但是會影響到多執行緒的正確性。

五. Volatile關鍵字

這裡呢Volatile的三個條件:

1.不保證原子性。

2.保證有序性。

3.保證可見性。

當用volatile修飾共享變數的時候,執行緒訪問到該變數的時候都回去主存中去取該變數的值,它的工作記憶體中的快取將失效,這樣就保證了每個執行緒訪問該變數的時候都是從主存中讀寫的。這就是為什麼使用Volatile關鍵字來修飾執行緒間共享變數。

六. 結束語

這些也是對JVM的一些小的探索,希望能給大家帶來一點小的幫助,如果喜歡的話請點個贊再走吧,感興趣的話就點 這裡 這個關注吧,之後我會繼續給大家帶來一下新的見解,或者把通俗易懂的語言來描述苦澀難懂的原理~

如果有什麼疑問或者見解請評論區留言,我會實時回覆的~

來了就點個贊吧~

歡迎關注

相關文章