java內存模型 JMM
627
2025-04-10
Java內存模型簡稱JMM(Java Memory Model),是Java虛擬機所定義的一種抽象規范,用來屏蔽不同硬件和操作系統的內存訪問差異,讓java程序在各種平臺下都能達到一致的內存訪問效果。
Java內存模型長成什么樣子呢?就是下圖的樣子:
這里需要解釋幾個概念:
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 = 0。
這段代碼是什么意思呢?很簡單,開啟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循環的可能性。
幾點補充:
1. 關于volatile的介紹,本文很多內容來自《深入理解Java虛擬機》這本書。有興趣的同學可以去看看。
2.本漫畫純屬娛樂,還請大家盡量珍惜當下的工作,切勿模仿小灰的行為哦。
—————END—————
喜歡本文的朋友們,歡迎長按下圖關注訂閱號程序員小灰,收看更多精彩內容
Java 任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。