在面試之時,不少面試官都喜歡問道,JMM清楚嗎?說說什麼是內存可見性,什麼是重排序?synchronized、volatile和final中的原理?等等諸如此類的問題。而網上一搜,巴啦啦一大堆,東西比較亂,也很難把面試官變相問題回答清楚。終於,下定決心給你們捋一捋JAVA簡化版的內存模型。java
Java的併發採用的是共享內存模型,Java線程之間的通訊老是隱式的,整個通訊過程對程序員來講徹底是透明的。程序員
JMM是Java內存模型,它決定了一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(Main Memory)中,每一個線程都有一個抽象的(真實不存在的)本地內存(Local Memory),本地內存存儲了該線程以讀、寫共享變量的副本。面試
知識點補充:(上述JMM所說的"共享變量"主要存在於java堆中)編程
JVM內存模型包括:
(1) 程序計數器。一塊很小的內存空間,用於記錄下一條要運行的指令。是線程私有的內存。
(2)java虛擬機棧。它和java線程同一時間建立,保存了局部變量、部分結果,並參與方法的調用和返回。是線程私有的內存。
(3)本地方法棧。它和java虛擬機棧的功能類似,主要爲Native方法服務。是線程私有的內存。
(4)java堆。爲全部建立的對象和數組分配內存空間。是線程共有的內存。
(5)方法區。也被稱爲永久區,與堆空間類似。是線程共有的內存。
複製代碼
在執行程序時,爲了提升性能,編譯器和處理器經常會對指令作重排序。然而在程序最終執行以前,還要作一個內存的重排序。數組
重排序可能會致使多線程程序出現內存可見性問題。請看代碼例子:bash
class ReorderEample {
int a = 0;
boolean flag = false;
//寫操做
public void writer() {
a = 1 ; // (1)
flag = true; //(2)
}
//讀操做
public void reader() {
if (flag) { //(3)
int i = a * a; //(4)
// 處理邏輯
}
}
}
複製代碼
flag變量是個標記,標識a是否已被寫入。假設有兩個線程A和B,A首先執行了寫操做writer(),隨後B接着執行讀操做reader()方法。那麼線程B在執行操做(4)時,是否能看到線程A在操做(1)時對共享變量a的寫入?答案是未必能看到。由於在重排序時,A線程可能先標識了flag變量,再對a變量進行寫入,可是在它們發生之間,B線程此時來讀了,該程序的語義被破壞了。以下程序執行時序圖:多線程
JMM中,被聲明成volatile的共享變量,線程經過排他鎖獲取這個變量,確保在線程中是可見的。爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。瞭解內存屏障詳情請看Java內存模型Cookbook(二)內存屏障併發
volatile遵循happens-before原則。請看以下代碼:app
class ReorderEample {
int a = 0;
volatile boolean flag = false;
//寫操做
public void writer() {
a = 1 ; // (1)
flag = true; //(2)
}
//讀操做
public void reader() {
if (flag) { //(3)
int i = a * a; //(4)
// 處理邏輯
}
}
}
複製代碼
假設線程A執行writer()方法以後,線程B執行reader()方法。根據happens-before原則,這個過程創建的happens-before關係分爲3類:函數
其happens-before創建關係圖以下:
對於上一個例子來講,這個例子只對flag變量增長了volatile聲明。A線程寫入一個volatile變量後,B線程讀同一個volatile變量。A線程在寫volatile變量以前全部可見的共享變量,在B線程讀同一個volatile變量後,當即對B線程可見。happens-before規則知識點補充:
(1)程序順序規則:一個線程中的每一個操做,happens-before於該線程中的任意後續操做
(2)監視器鎖規則:對一個線程的解鎖,happens-before於隨後對這個線程的加鎖
(3)volatile變量規則:對一個volatile域的寫,happens-before於後續對這個volatile域的讀
(4)傳遞性:若是A happens-before B ,且 B happens-before C, 那麼 A happens-before C
(5)start()規則:若是線程A執行操做ThreadB_start()(啓動線程B) , 那麼A線程的ThreadB_start()happens-before 於B中的任意操做
(6)join()原則:若是A執行ThreadB.join()而且成功返回,那麼線程B中的任意操做happens-before於線程A從ThreadB.join()操做成功返回。
(7)interrupt()原則: 對線程interrupt()方法的調用先行發生於被中斷線程代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測是否有中斷髮生
(8)finalize()原則:一個對象的初始化完成先行發生於它的finalize()方法的開始。
複製代碼
synchronized內存語義與volatile內存語義相似,在Java併發編程機制中,鎖除了讓臨界區互斥以外,還可讓釋放鎖的線程向獲取同一個鎖的線程發送消息。它的核心底層就是使用一個volatile聲明的state變量來維護同步狀態。
鎖也遵循happens-before規則。請看以下代碼:
class MonitorExample{
int a = 0;
//寫操做
public synchronized void writer() { //(1)
a ++; //(2)
} //(3)
//讀操做
public synchronized void reader() { //(4)
int i = a; //(5)
//處理邏輯
} //(6)
}
複製代碼
假設線程A執行writer()方法,隨後線程B執行reader()方法。根據happens-before規則,這個過程包含的happens-before關係能夠分爲3類:
其happens-before創建關係圖以下:
線程A釋放了鎖以後,隨後線程B獲取同一個鎖。由於 (2) happens-before (5),因此線程A在釋放鎖以前全部可見的共享變量在線程B獲取同一個鎖以後對於B線程都變得可見。在JMM中,經過內存屏障禁止編譯器把final域的寫重排序到構造函數以外。所以,在對象引用爲任意線程可見以前,對象的final域已經被正確初始化(不爲null的狀況)了。 對於final域,編譯器和處理器遵循兩個重排序規則:
下面經過兩個示例來講明這兩個規則。
public class FinalExample {
int i; //普通變量
final int j; //final變量
static FinalExample obj;
public FinalExample(int j) { //構造函數
i = 1; //寫普通域
this.j = j; //寫final域
}
public static void writer() { //寫線程A執行
obj = new FinalExample(2);
}
public static void reader() { //讀線程B執行
FinalExample object = obj; //讀引用對象
int a = object.i; //讀普通域
int b = object.j; //讀final域
}
}
複製代碼
寫普通域的操做被編譯器重排序到了構造函數以外,讀線程B錯誤地讀取了普通變量i初始化以前的值。而寫final域操做後,被寫final域的重排序規則「限定」在了構造函數以內,讀線程B正確地讀取了final變量初始化的值。執行時序圖以下:
public class FinalReferenceExample {
final int[] intArray;
static FinalReferenceExample obj;
public FinalReferenceExample() { //構造函數
intArray = new int[1]; //(1)
intArray[0] = 1; //(2)
}
public static void writeOne() { //寫線程A執行
obj = new FinalReferenceExample(); //(3)
}
public static void writeTwo() { //寫線程B執行
obj.intArray[0] = 2; //(4)
}
public static void reader () { //讀線程C執行
if (obj != null) { //(5)
int temp = obj.intArray[0]; //(6)
}
}
}
複製代碼
首先線程A執行writeOne()方法,執行完後線程B執行writeTwo方法,執行完後線程C執行reader方法。操做(1)對final域的寫入,操做(2)是對final域引用的對象的成員寫入,操做(3)是把被構造的對象的引用賦值給某個引用變量。這裏除了(1)和(3)不能重排序,(2)和(3)也不能重排序。所以,該程序的線程執行時序不可知,由於寫線程B和讀線程C之間存在數據競爭。
參考:《Java併發編程的藝術》 方騰飛 魏鵬 程曉明 《深刻理解Java虛擬機》 周志明
若是這篇文章對你有用,請你點個贊吧!你的支持是我分享的動力。