一、什麼是JMM
Java線程內存模型跟CPU緩存模型類型,是基於CPU緩存模型來建立的,Java線程內存模型是標準化的,屏蔽掉了底層不同計算機的區別。
二、JMM數據原子操作
- read(讀取):從主內存讀取數據;
- load(載入):將主內存讀取到的數據寫入工作內存;
- use(使用):從工作內存讀取數據來計算;
- assign(賦值):將計算好的值重新賦值到工作內存中;
- store(存儲):將工作內存數據寫入主內存;
- write(寫入):將store過去的變量值賦值給主內存中的變量;
- lock(鎖定):將主內存變量加鎖,標識爲線程獨佔狀態;
- unlock(解鎖):將主內存變量解鎖,解鎖後其他線程可以鎖定該變量
三、JMM緩存不一致問題
cpu從主內存讀取數據到高速緩存,會在總線對這個數據加鎖,這樣其他CPU沒法去讀或寫這個數據,直到這個CPU使用完數據釋放鎖之後其他cpu才能讀取該數據。
多個cpu從主內存讀取同一個數據到各自的高速緩存,當其中某個cpu修改了緩存裏的數據,該數據會馬上同步回主內存,其他cpu通過總線嗅探機制可以感知到數據的變化從而將自己緩存裏的數據失效。
結合上圖,我們接着來進一步理解緩存一致性協議,volatile的實現跟它有關,所以要好好理解這個協議。以下是volatile底層MESI緩存一致性協議保證共享變量父本在內存可見性的大致流程。
(1)假設線程1的那個CPU開啓了緩存一致性協議,那麼線程1的CPU會對總線進行監聽。
(2)當線程2這個CPU對initflag這個變量的值進行了修改,在store回主內存的過程中會經過總線,此時監聽到initflag被修改後,線程1會將自己工作內存中的initflag置爲失效。
(3)initflag修改後的值被write到主內存中。
(4)線程1發現自己工作內存中的值被置爲無效後,會重新從主內存中read值出來並且load回工作內存。
注:volatile的實現是C語言,所以在Idea工具裏面我們按住Ctrl鍵+鼠標點擊是進不去看源碼的。通過查看Java代碼生成的彙編代碼,我們可以知道,用volatile修飾的Java代碼,在彙編代碼中會有對應的lock指令,通過該指令就會開啓MESI緩存一致性協議和cpu總線的嗅探機制。
四、Volatile可見性底層實現原理
底層實現主要是通過彙編lock前綴指令,它會鎖定這塊內存區域的緩存(緩存行鎖定)並寫回到主內存。
IA-32架構軟件開發者手冊對lock指令的解釋:
1)會將當前處理器緩存行的數據立即寫回到系統內存;
2)這個寫回內存的操作會引起在其他CPU裏緩存了該內存地址的數據無效(MESI協議)。
五、Volatile可見性、原子性與有序性
- 併發編程三大特性:可見性、原子性、有序性。
- volatile保證可見性與有序性,但是不保證原子性,保證原子性需要藉助synchronized這樣的鎖機制。
下面我們來看一道阿里的面試題
public class VolatileActomicTest {
private static volatile int num = 0;
public static void increase(){
num++;
}
public static void main(String[] args) throws InterruptedException{
Thread[] threads = new Thread[2];
for(int i = 0; i < 2; i++){
threads[i] = new Thread(new Runnable() {
public void run() {
for(int i = 0; i < 1000; i++){
increase();
}
}
});
threads[i].start();
}
for(Thread t : threads){
t.join();
}
System.out.println(num);
}
}
以上是代碼及運行結果,從結果我們可以看出運行結果是小於等於2000,證明了volatile不能保證原子性。
接着我們通過這張內存模型的圖片對這道程序進行分析。在理想情況下,我們會以爲結果就是2000,啓動兩個線程,每個線程分別增加1000;但是這些assign等數據原子操作可能存在兩個線程同時進行同一操作,或者先後操作的情況。當線程1和線程2都use變量進行加1操作,並且都assign回工作內存後,線程1先進行了store操作,此時通過嗅探機制監聽,線程2的工作內存中的num會被置爲無效。接着,線程2會重新去主內存讀取num的值(線程1已經將num=1write到主內存),接着線程2再根據取到的值再次進行循環自增的操作。到目前爲止,線程1進行了一次自增,線程2進行了兩次自增,但此時num的值是2,而不是3。這就是爲什麼程序的執行結果大多數時候不是2000而是小於2000的原因。