volatile域淺析
內存模型的相關概念
計算機中執行程序時,每條指令都是在CPU中執行,執行指令的過程必然會涉及到數據的讀取和寫入。而程序運行時的數據是存放在主存(物理內存)中,由於CPU的讀寫速度遠遠高於內存的速度,如果CPU直接和內存交互,會大大降低指令的執行速度,所以CPU裏面就引入了高速緩存。
腦補當初學習OS時的圖 CPU->內存 CPU->寄存器->內存
也就是說程序運行時,會將運算所需要的數據從主存中複製一份到高速緩存,CPU進行計算的時候可以直接從高速緩存讀取和寫入,當運算結束時,在將高速緩存中的數據刷新到主存。
但是如果那樣必須要考慮,在多核CPU下數據的一致性問題怎麼保證?比如i=i+1
,當線程執行這條時,會先從主存中讀取i的值,然後複製一份到高速緩存,然後CPU執行指令對i進行加1操作,然後將數據寫入高速緩存,最後將高速緩存中i最新的值刷新到主存當中。在單線程下這段代碼運行不會存在問題,但如果在多線程下多核CPU中,每個CPU都有自己的高速緩存,可能存在下面一種情況:初始時,兩個線程分別讀取i的值存入各自所在的CPU的高速緩存當中,然後線程1進行加1操作,然後把i的最新值1寫入到內存。此時線程2的高速緩存當中i的值還是0,進行加1操作之後,i的值為1,然後線程2把i的值寫入內存。最終結果i的值是1,而不是2。這就是著名的緩存一致性問題。通常稱這種被多個線程訪問的變量為共享變量。
為了解決緩存不一致性問題,通常來說有以下2種解決方法:
-
通過在總線加LOCK#鎖的方式
-
通過緩存一致性協議
原子性可見性有序性
並發編程中,通常會考慮的三個問題原子性問題、可見性問題、有序性問題。
(1)原子性:程序中的單步操作或多步操作要麼全部執行並且執行的過程中不能被打斷,要麼都不執行。
如果程序中不具備原子性會出現哪些問題?
轉賬操作就是一個很好的代表,如果轉賬的過程中被中斷,錢轉出去了,由於中斷,收賬方卻沒有收到。
(2)可見性:內存可見性,指的是線程之間的可見性,當一個線程修改了共享變量時,另一個線程可以讀取到這個修改後的值。
//線程1執行的代碼
int i = 0;
i = 10;
//線程2執行的代碼
j = i;
倘若線程1從主存中讀取了i的值並複製到CPU高速緩存,然後對i修改為10,這時CPU高速緩存中的i值為10,在沒有將高速緩存中的值刷新到主存中時,線程2讀取到的值還是0,它看不到i值的變化,這就是可見性問題。
Java提供了Volatile關鍵字來保證可見性,當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
而普通的共享變量不能保證可見性,因為普通共享變量被修改之後,什麼時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。
另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
(3)有序性:程序執行的順序按照代碼的先後順序執行。
實際是這樣嗎?
int i = 0; //[1]
int a,b; //[2]
[2]一定會在[1]之後執行嗎?不一定,在JVM中,有可能會發生指令重排序(Instruction Reorder)。如果[1]、[2]中有相互依賴,比如[2]中的數據依賴於[1]的結果,那麼則不會發生指令重排序。
什麼是指令重排序?
一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
指令重排對於提⾼CPU處理性能⼗分必要。雖然由此帶來了亂序的問題,但是這點犧牲是值得的。
指令重排可以保證串⾏語義⼀致,但是沒有義務保證多線程間的語義也⼀致。所以在多線程下,指令重排序可能會導致⼀些問題。
Java內存模型的抽象結構
JVM可以看做是一個有OS架構的處理機,他也有自己的內存和處理器,它的內存和之前討論的沒有什麼太大的差異。
Java運行時內存的劃分如下:
對於每⼀個線程來說,棧都是私有的,而堆是共有的。也就是說在棧中的變量(局部變量、⽅法定義參數、異常處理器參數)不會在線程之間共享,也就不會有內存可⻅性(下⽂會說到)的問題,也不受內存模型的影
響。⽽在堆中的變量是共享的,本⽂稱為共享變量。所以內存可見性針對的是共享變量。
1、既然堆是共享的,為什麼在堆中會有內存不可⻅問題?
Java內存模型規定所有的變量都是存在主存當中(類似於前面說的物理內存),每個線程都有自己的工作內存(類似於前面的高速緩存)。線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。並且每個線程不能訪問其他線程的工作內存。
線程之間的共享變量存在主內存中,每個線程都有⼀個私有的本地內存,存儲了該線程以讀、寫共享變量的副本。本地內存是Java內存模型的⼀個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器等。
Java線程之間的通信由Java內存模型(簡稱JMM)控制,從抽象的⻆度來說,JMM定義了線程和主內存之間的抽象關係。JMM的抽象示意圖如圖所示:
從圖中可以看出:
- 所有共享變量都存在主內存中。
- 每個線程都保存了一份該線程使用到的共享變量的副本。
- 線程A與線程B之間的通信必須通過主存。
2、JMM與Java內存區域劃分的區別與聯繫
-
區別
JMM是抽象的,他是⽤來描述⼀組規則,通過這個規則來控制各個變量的訪問⽅式,圍繞原⼦性、有序性、可⻅性等展開的。⽽Java運⾏時內存的劃分是具體的,是JVM運⾏Java程序時,必要的內存劃分。
-
聯繫
都存在私有數據區域和共享數據區域。⼀般來說,JMM中的主內存屬於共享數據區域,他是包含了堆和⽅法區;同樣,JMM中的本地內存屬於私有數據區域,包含了程序計數器、本地⽅法棧、虛擬機棧。
原子性、可見性、有序性
Java內存模型具備一些先天的「有序性」,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
在Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽各個硬件平台和操作系統的內存訪問差異,以實現讓Java程序在各種平台下都能達到一致的內存訪問效果。那麼Java內存模型規定了哪些東西呢,它定義了程序中變量的訪問規則,往大一點說是定義了程序執行的次序。注意,為了獲得較好的執行性能,Java內存模型並沒有限制執行引擎使用處理器的寄存器或者高速緩存來提升指令執行速度,也沒有限制編譯器對指令進行重排序。也就是說,在java內存模型中,也會存在緩存一致性問題和指令重排序的問題。
volatile的內存語義
在Java中,volatile關鍵字有特殊的內存語義。volatile主要有以下兩個功能:
- 保證變量的內存可⻅性
- 禁⽌volatile變量與普通變量重排序(JSR133提出,Java 5 開始才有這個「增強的volatile內存語義「)
內存可見性
所謂內存可見性,指的是當一個線程對volatile
修飾的變量進行過寫操作時,JMM會立即把線程對應的本地內存中的共享變量的值刷新到主內存;當一個線程對volatile
修飾的變量進行讀操作時,JMM會立即把該線程對應的本地內存置為無效,從內存中從新讀取共享變量的值。
禁止重排序
JMM是通過內存屏障來限制處理器對指令的重排序的。
什麼是內存屏障?硬件層面,內存屏障分兩種:讀屏障(Load Barrier)和寫屏障(Store Barrier)。內存屏障有兩個作用:
- 阻止屏障兩側的指令重排序
- 強制把寫緩衝區/⾼速緩存中的臟數據等寫回主內存,或者讓緩存中相應的數據 失效。
通俗說,通過內存屏障,可以防止指令重排序時,不會將屏障後面的指令排到之前,也不會將屏障之前的指令排到之後。
Volatile關鍵字的應用場景
單例模式下的Double-Check(雙重鎖檢查)
public class Singleton {
public static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { //[1]
synchronized (Singleton.class) {
instance = new Singleton(); //[2]
}
}
return instance;
}
}
如果這裡的變量沒有使用volatile關鍵字,那麼有可能就會發生錯誤。
[2]實例化對象的過程可以分為分配內存、初始化對象、引用賦值。
instance = new Singleton(); // [1]
// 可以分解為以下三個步驟
1 memory=allocate();// 分配內存 相當於c的malloc
2 ctorInstanc(memory) //初始化對象
3 s=memory //設置s指向剛分配的地址
// 上述三個步驟可能會被重排序為 1-3-2,也就是:
1 memory=allocate();// 分配內存 相當於c的malloc
3 s=memory //設置s指向剛分配的地址
2 ctorInstanc(memory) //初始化對象
如果一旦發生了上述的重排序,當程序執行了1和3,這時線程A執行了if判斷,判定instance不為空,然後直接返回了一個未初始化的instance。