掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
在之前的文章 深入分析 Synchronized 原理 介紹了 Synchronized是一種鎖的機(jī)制,存在阻塞和性能的問題,而 volatile 是 java 虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制,volatile 主要提供修飾共享變量賦予 “可見性” 和 “有序性”。從簡(jiǎn)單的 Demo 引出我們今天的主題 -- volatile。

Demo -- 多線程共享對(duì)象 控制執(zhí)行開關(guān)。
public class Demo {
private static boolean switchStatus = false;
public static void main(String[] args) {
new Thread(() -> {
System.out.println("開始工作");
while (!switchStatus) ;
System.out.println("結(jié)束工作");
}).start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
switchStatus = true;
System.out.println("命令停止工作");
}
}本意是想通過 switchStatus 作為控制工作線程的開關(guān),但是實(shí)際執(zhí)行后,會(huì)發(fā)現(xiàn)結(jié)果并沒有按照預(yù)期 輸出"結(jié)束工作",而是失聯(lián)了一樣停不下來了,在死循環(huán)中出不來了。
但是如果在上面的 Demo 進(jìn)行稍微的修改即可滿足預(yù)期: private static volatile boolean switchStatus = false; 此時(shí)符合預(yù)期關(guān)閉開關(guān)時(shí),工作線程也隨之關(guān)閉了。接下我會(huì)針對(duì)這2個(gè)現(xiàn)象原理進(jìn)行解答,為了讀者更好的理解,得先引入幾個(gè)知識(shí)點(diǎn)(計(jì)算機(jī)內(nèi)存模型、JMM-Java 內(nèi)存模型)。
為了更好地理解后續(xù) JMM 和 volatile,我們先了解下計(jì)算機(jī)內(nèi)存模型,簡(jiǎn)單地介紹下:
程序執(zhí)行時(shí),CPU接收到指令 需要進(jìn)行計(jì)算時(shí),讀取所需要的數(shù)據(jù),會(huì)先嘗試從 CPU Cache 中獲取,若沒有再從主內(nèi)存中獲取,計(jì)算完成后,將結(jié)果寫入 CPU Cache ,若沒有特殊指令的情況下,會(huì)根據(jù)操作系統(tǒng)自身定義的時(shí)間 一段時(shí)間會(huì)將 CPU Cache 刷新到主內(nèi)存中(未被volatile 修飾的普通變量);當(dāng)然遇到特殊的指令會(huì)將 CPU Cache 刷新到主內(nèi)存中(被volatile 修飾的變量 就是依賴這個(gè)特性實(shí)現(xiàn)可見性)。
CPU和其他功能部件是通過總線通信的,如果在總線加LOCK#鎖,那么在鎖住總線期間,其他CPU是無法訪問內(nèi)存,這樣一來,效率就比較低了。因此需要進(jìn)行優(yōu)化,細(xì)化控制鎖的粒度,我們只需要保證,對(duì)于被多個(gè)CPU緩存的同一份數(shù)據(jù)是一致的就行,所以引入了緩存鎖,他的核心機(jī)制就是緩存一致性協(xié)議。
為了達(dá)成數(shù)據(jù)訪問的一致性,需要各個(gè)處理器在訪問內(nèi)存時(shí),遵循一些協(xié)議,在讀寫時(shí)根據(jù)協(xié)議來操作,常見的協(xié)議有,MSI,MESI,MOSI等等,最常見的就是MESI協(xié)議;MESI表示緩存行的四種狀態(tài)(modify、 Exclusive、Shared、 Invalid)。
如何保證當(dāng)前處理器的內(nèi)部緩存、主內(nèi)存和其他處理器的緩存數(shù)據(jù)在總線上保持一致的?多處理器總線嗅探。
在多處理器下,為了保證各個(gè)處理器的緩存是一致的,就會(huì)實(shí)現(xiàn)緩存緩存一致性協(xié)議,每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己的緩存值是不是過期了,如果處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存行設(shè)置無效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)庫讀到處理器緩存中。
舉個(gè)例子:
# 初始值
i = 0;
# 線程A 和 線程B同時(shí)進(jìn)行操作
i = i + 1;
首先,執(zhí)行線程A從主內(nèi)存中讀取到 i=0,到工作內(nèi)存。然后在工作內(nèi)存中,賦值 i+1,工作內(nèi)存就得到 i=1,最后把結(jié)果寫回主內(nèi)存。如果是單線程的話,該語句執(zhí)行是沒問題的。但是在多線程的情況下,線程B的本地工作內(nèi)存和線程A的工作內(nèi)存讀取的時(shí)間相同都是 i=0,但是線程A將 i=1寫入主內(nèi)存中,線程B不知情的情況下,也做了 i+1 的操作,此時(shí)就出現(xiàn)可見性帶來問題了:連續(xù)2次的 i=i+1 最終的結(jié)果是1。
在之前的文章 深入分析 Synchronized 原理 已經(jīng)介紹過 原子性、可見性、有序性定義,這里也就不展開說了。
先說結(jié)論:依賴于 CPU 緩存一致性協(xié)議 和 內(nèi)存屏障 解決了可見性的問題。
正常來說,volatile 基于緩存一致性協(xié)議就應(yīng)該可以實(shí)現(xiàn)可見性(在上面已經(jīng)介紹過 緩存一致性協(xié)議和嗅探技術(shù)),但是由于 Java 為了提高性能允許重排序(編譯器重排序 和 處理器重排序),因此需要通過內(nèi)存屏障來防止重排序,來保證每個(gè)線程執(zhí)行的每個(gè)指令有一定的順序性。
java的內(nèi)存屏障通常所謂的四種即 LoadLoad、StoreStore、 LoadStore、StoreLoad 實(shí)際上也是上述兩種的組合,完成一系列的屏障和數(shù)據(jù)同步功能。
java 中 DLC單例模式 大家應(yīng)該很熟悉了,只不過大家是否有注意到 uniqueInstance 被 volatile 修飾的作用嗎? 就是為了防止指令重排。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getInstance() {
if (uniqueInstance != null) {
synchronized (Singleton.class) {
if (uniqueInstance != null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}初始化一個(gè)類,會(huì)產(chǎn)生多條匯編指令,總結(jié)下來主要執(zhí)行下面三點(diǎn):
理想的狀態(tài)下:1 -> 2 -> 3,但是 Java 為了提高性能允許重排序,可能會(huì)將初始化一個(gè)類的順序進(jìn)行變化,比如:1 -> 3 -> 2,這種情況下就可能會(huì)出現(xiàn)NPE,修飾了volatile 防止重排序,避免獲取到 uniqueInstance 未初始化完成,導(dǎo)致NPE
最后簡(jiǎn)單總結(jié)下:volatile 在指令之間插入內(nèi)存屏障 + 緩存一致性協(xié)議,保證按照特定順序執(zhí)行和某些變量的可見性。volatile 通過 內(nèi)存屏障通知 CPU 和編譯器阻止指令重排優(yōu)化來維持有序性。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流