Java多線程-帶你認識Java內存模型,內存分區,從原理剖析Volatile關鍵字

寫在前面(語句修改版)

讀完本篇文章你將知道:

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

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

來了就點個贊吧~

歡迎關注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章