轉載地址:https://zhuanlan.zhihu.com/p/56191979java
————— 次日 —————緩存
————————————安全
Java內存模型簡稱JMM(Java Memory Model),是Java虛擬機所定義的一種抽象規範,用來屏蔽不一樣硬件和操做系統的內存訪問差別,讓java程序在各類平臺下都能達到一致的內存訪問效果。多線程
Java內存模型長成什麼樣子呢?就是下圖的樣子:併發
這裏須要解釋幾個概念:app
1.主內存(Main Memory)優化
主內存能夠簡單理解爲計算機當中的內存,但又不徹底等同。主內存被全部的線程所共享,對於一個共享變量(好比靜態變量,或是堆內存中的實例)來講,主內存當中存儲了它的「本尊」。操作系統
2.工做內存(Working Memory)線程
工做內存能夠簡單理解爲計算機當中的CPU高速緩存,但又不徹底等同。每個線程擁有本身的工做內存,對於一個共享變量來講,工做內存當中存儲了它的「副本」。翻譯
線程對共享變量的全部操做都必須在工做內存進行,不能直接讀寫主內存中的變量。不一樣線程之間也沒法訪問彼此的工做內存,變量值的傳遞只能經過主內存來進行。
以上說的這些可能有點抽象,你們來看看下面這個例子:
對於一個靜態變量
static int s = 0;
線程A執行以下代碼:
s = 3;
那麼,JMM的工做流程以下圖所示:
經過一系列內存讀寫的操做指令(JVM內存模型共定義了8種內存操做指令,之後會細講),線程A把靜態變量 s=0 從主內存讀到工做內存,再把 s=3 的更新結果同步到主內存當中。從單線程的角度來看,這個過程沒有任何問題。
這時候咱們引入線程B,執行以下代碼:
System.out.println("s=" + s);
引入線程B之後,當線程A首先執行,更大的多是出現下面狀況:
此時線程B從主內存獲得的s值是3,理所固然輸出 s=3,這種狀況不難理解。可是,有較小的概率出現另外一種狀況:
由於工做內存所更新的變量並不會當即同步到主內存,因此雖然線程A在工做內存當中已經把變量s的值更新成3,可是線程B從主內存獲得的變量s的值仍然是0,從而輸出 s=0。
volatile關鍵字具備許多特性,其中最重要的特性就是保證了用volatile修飾的變量對全部線程的可見性。
這裏的可見性是什麼意思呢?當一個線程修改了變量的值,新的值會馬上同步到主內存當中。而其餘線程讀取這個變量的時候,也會從主內存中拉取最新的變量值。
爲何volatile關鍵字能夠有這樣的特性?這得益於java語言的先行發生原則(happens-before)。先行發生原則在維基百科上的定義以下:
In computer science, the happened-before relation is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that, even if those events are in reality executed out of order (usually to optimize program flow).
翻譯結果以下:
在計算機科學中,先行發生原則是兩個事件的結果之間的關係,若是一個事件發生在另外一個事件以前,結果必須反映,即便這些事件其實是亂序執行的(一般是優化程序流程)。
這裏所謂的事件,實際上就是各類指令操做,好比讀操做、寫操做、初始化操做、鎖操做等等。
先行發生原則做用於不少場景下,包括同步鎖、線程啓動、線程終止、volatile。咱們這裏只列舉出volatile相關的規則:
對於一個volatile變量的寫操做先行發生於後面對這個變量的讀操做。
回到上述的代碼例子,若是在靜態變量s以前加上volatile修飾符:
volatile static int s = 0;
線程A執行以下代碼:
s = 3;
這時候咱們引入線程B,執行以下代碼:
System.out.println("s=" + s);
當線程A先執行的時候,把s = 3寫入主內存的事件一定會先於讀取s的事件。因此線程B的輸出必定是s = 3。
這段代碼是什麼意思呢?很簡單,開啓10個線程,每一個線程當中讓靜態變量count自增100次。執行以後會發現,最終count的結果值未必是1000,有可能小於1000。
使用volatile修飾的變量,爲何併發自增的時候會出現這樣的問題呢?這是由於count++這一行代碼自己並非原子性操做,在字節碼層面能夠拆分紅以下指令:
getstatic //讀取靜態變量(count)
iconst_1 //定義常量1
iadd //count增長1
putstatic //把count結果同步到主內存
雖然每一次執行 getstatic 的時候,獲取到的都是主內存的最新變量值,可是進行iadd的時候,因爲並非原子性操做,其餘線程在這過程當中極可能讓count自增了不少次。這樣一來本線程所計算更新的是一個陳舊的count值,天然沒法作到線程安全:
所以,何時適合用volatile呢?
1.運行結果並不依賴變量的當前值,或者可以確保只有單一的線程修改變量的值。
2.變量不須要與其餘的狀態變量共同參與不變約束。
第一條很好理解,就是上面的代碼例子。第二條是什麼意思呢?能夠看看下面這個場景:
volatile static int start = 3;
volatile static int end = 6;
線程A執行以下代碼:
while (start < end){
//do something
}
線程B執行以下代碼:
start+=3;
end+=3;
這種狀況下,一旦在線程A的循環中執行了線程B,start有可能先更新成6,形成了一瞬間 start == end,從而跳出while循環的可能性。
什麼是指令重排?
指令重排是指JVM在編譯Java代碼的時候,或者CPU在執行JVM字節碼的時候,對現有的指令順序進行從新排序。
指令重排的目的是爲了在不改變程序執行結果的前提下,優化程序的運行效率。須要注意的是,這裏所說的不改變執行結果,指的是不改變單線程下的程序執行結果。
然而,指令重排是一把雙刃劍,雖然優化了程序的執行效率,可是在某些狀況下,會影響到多線程的執行結果。咱們來看看下面的例子:
boolean contextReady = false;
在線程A中執行:
context = loadContext();
contextReady = true;
在線程B中執行:
while( ! contextReady ){
sleep(200);
}
doAfterContextReady (context);
以上程序看似沒有問題。線程B循環等待上下文context的加載,一旦context加載完成,contextReady == true的時候,才執行doAfterContextReady 方法。
可是,若是線程A執行的代碼發生了指令重排,初始化和contextReady的賦值交換了順序:
boolean contextReady = false;
在線程A中執行:
contextReady = true;
context = loadContext();
在線程B中執行:
while( ! contextReady ){
sleep(200);
}
doAfterContextReady (context);
這個時候,極可能context對象尚未加載完成,變量contextReady 已經爲true,線程B直接跳出了循環等待,開始執行doAfterContextReady 方法,結果天然會出現錯誤。
須要注意的是,這裏java代碼的重排只是爲了簡單示意,真正的指令重排是在字節碼指令的層面。