用 volatile 修飾的變量能夠保證其對(duì)所有線程的可見性,要理解這一點(diǎn),我們首先需要了解 Java 的內(nèi)存模型。
1. Java 內(nèi)存模型
Java 內(nèi)存模型分為主內(nèi)存和工作內(nèi)存。
主內(nèi)存是對(duì)所有線程所共享的,此外每個(gè)線程有自己的工作內(nèi)存,工作內(nèi)存不共享。
線程在工作時(shí),從主內(nèi)存中拷貝所需變量到自己的工作內(nèi)存中。
線程對(duì)變量的所有操作,都必須在工作內(nèi)存中進(jìn)行,不能直接操作主存中的變量,也不能直接訪問其他線程的工作內(nèi)存。
線程間變量值的傳遞需要通過(guò)主內(nèi)存進(jìn)行,何時(shí)將工作內(nèi)存中的變量同步到主內(nèi)存,由 JVM 控制。

基于此種內(nèi)存模型,在多線程中會(huì)產(chǎn)生臟讀,即讀到非最新的數(shù)據(jù)。
譬如,有1個(gè)共享變量:
int i = 0;
線程A和線程B同時(shí)執(zhí)行以下操作:
i++;
我們期望的結(jié)果為 2,但實(shí)際結(jié)果可能為 1 也可能是 2。
我們分析一下線程的執(zhí)行過(guò)程:首先從主內(nèi)存中拷貝 變量i 到自己的工作內(nèi)存,對(duì)工作內(nèi)存中的 變量i 副本進(jìn)行 +1 操作,將 i 的最新值寫入到主內(nèi)存中。
當(dāng) 2 個(gè)線程同時(shí)執(zhí)行上述代碼時(shí),可能存在以下一種情況:線程A從主存中讀取了 變量i 到工作內(nèi)存中,并對(duì) i 進(jìn)行 +1 操作。在線程A將最新值 i=1 寫入到主存前,此時(shí)線程B從主存中讀取了 變量i,此時(shí) i 仍為 0。線程A、B分別將操作后的 變量i 的值同步到主存,最終在主存中 i = 1。
在上述例子中,線程A和B的工作內(nèi)存是相互隔離、不可訪問的,即不可見。
那么 volatile 能實(shí)現(xiàn)的可見性是什么呢,是能讓1個(gè)線程的工作內(nèi)存變成共享的嗎?并非如此,我們看下 Java中可見性的定義。
2. Java 中的可見性
可見性是指當(dāng)一個(gè)線程修改共享變量,其他線程下次讀取到的將是該共享變量的最新值。
上文說(shuō)到線程的工作內(nèi)存對(duì)其他線程是隔離的,那么如何保證其他線程讀到的是最新值呢?
事實(shí)上,當(dāng)一個(gè)共享變量用 volatile 關(guān)鍵字修飾時(shí),它會(huì)保證修改的值會(huì)被立即更新到主存中,同時(shí)其他線程的工作內(nèi)存中該共享變量的緩存將失效,當(dāng)線程下次讀取該變量時(shí),將強(qiáng)制主存中讀取最新值。
接下來(lái)從硬件的角度,簡(jiǎn)要說(shuō)下 volatile 的實(shí)現(xiàn)原理。
3. volatile 實(shí)現(xiàn)原理
Java 虛擬機(jī)規(guī)范定義了 Java 內(nèi)存模型來(lái)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存差異。
但是為了實(shí)現(xiàn)更好的執(zhí)行性能,Java 內(nèi)存模型沒有限制執(zhí)行引擎使用CPU的特定緩存器或緩存來(lái)和主內(nèi)存交互。
為方便理解,我們將 Java 內(nèi)存模型中的主內(nèi)存類比為 RAM(系統(tǒng)內(nèi)存),工作內(nèi)存類比為 CPU的高速緩存。
實(shí)際上,工作內(nèi)存并非獨(dú)立存在的一段內(nèi)存空間,它是對(duì)CPU的寄存器、高速緩存及其他硬件的抽象描述。
我們看下 CPU 和 系統(tǒng)內(nèi)存間的交互:

對(duì)于多核心處理器,每個(gè)處理器都有自己的高速緩存,用于緩存計(jì)算中間結(jié)果。當(dāng)不同核心上執(zhí)行的運(yùn)算任務(wù)涉及到同一塊內(nèi)存區(qū)域時(shí),就有可能出現(xiàn)緩存不一致的問題。
我們看看 Java 虛擬機(jī)是如何解決這個(gè)問題的,當(dāng)對(duì)用 volatile 修飾的變量進(jìn)行了寫操作時(shí),JVM 會(huì)向 CPU 發(fā)送一條 Lock 前綴的指令,該指令將做以下2件事情:
- 將 CPU 高速緩存中的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存;
- 如果其他 CPU 核心緩存了該數(shù)據(jù),將其置為失效。
其中 操作2 是通過(guò)緩存一致性協(xié)議實(shí)現(xiàn)的: 每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù),檢查自己緩存中的數(shù)據(jù)是否過(guò)期,當(dāng)處理器發(fā)現(xiàn)高速緩存中的數(shù)據(jù)對(duì)應(yīng)的內(nèi)存地址被修改,會(huì)將該緩存數(shù)據(jù)置為失效,當(dāng)處理器下次訪問該內(nèi)存地址數(shù)據(jù)時(shí),將強(qiáng)制重新從系統(tǒng)內(nèi)存中讀取。
4. volatile 的使用說(shuō)明
我們已經(jīng)解釋了 volatile 的實(shí)現(xiàn)原理和作用,那么它能避免文章開頭提到的臟讀問題嗎?答案是并不能,原因是 volatile 不能實(shí)現(xiàn)原子性操作。
原子性操作是不可再拆分的操作,要么執(zhí)行,要么不執(zhí)行。
原子操作不會(huì)被線程調(diào)度機(jī)制打斷,不需要 synchronized。
i++ 雖然僅包含一行語(yǔ)句,但實(shí)際上它進(jìn)行了 3 項(xiàng)操作:
- 從內(nèi)存中讀取 i 的至;
- 對(duì) i 進(jìn)行 +1 操作
- 將 i 的新值寫入到內(nèi)存中。
即便 volatile 能夠保證 線程A 進(jìn)行了 i+1 操作后,i 的新值將被立即更新到主存。但在 i 寫入到主存前,可能線程B已經(jīng)讀取了 i 值,此時(shí) i 仍為 0。在線程A 將 i 的新值寫到主存后,線程B 的工作內(nèi)存中 i 的緩存將失效,但此時(shí)線程B已無(wú)需再讀取 i 值。所以兩次 +1 操作后最終 i=1。
執(zhí)行以下代碼能證明上文這一點(diǎn):
public class VolatileDemo {
private volatile int i;
public void inc() {
i++;
}
public int getI() {
return i;
}
public static void main(String[] args) {
final VolatileDemo test = new VolatileDemo();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
test.inc();
}
}).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(test.getI());
}
}
執(zhí)行多次,發(fā)現(xiàn)控制臺(tái)打印的 i 值總是小于預(yù)期結(jié)果 10 * 1000 = 10000.
若要達(dá)到預(yù)期效果,則必須讓 i++ 變?yōu)樵硬僮?,這就需要通過(guò) synchronized 實(shí)現(xiàn),將 inc() 函數(shù)改為:
public synchronized void inc() {
i++;
}
則每次執(zhí)行結(jié)果都為 10000.
由于 volatile 無(wú)法保證操作的原子性,在多線程場(chǎng)景下使用 volatile 需要保證以下2點(diǎn):
- 對(duì)變量的寫操作不依賴于當(dāng)前值;
- 該變量不會(huì)與其他變量被一起納入到不變性條件中(譬如下界 <= 上界)。
下面舉幾個(gè)應(yīng)用場(chǎng)景:
- 用作狀態(tài)標(biāo)記
volatile boolean shutdownFlag;
public void shutdown() {
shutdownFlag = true;
}
public void doWork() {
while (!shutdownFlag) {
doSomething();
}
}
在 shutdown() 方法中,shutdownFlag = true 的賦值操作,與 shutdownFlag的當(dāng)前值無(wú)關(guān)。而上文中的 i++ 操作,i 的新值依賴于當(dāng)前值。
- 雙重檢查
class Singleton {
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
在1個(gè)線程執(zhí)行完語(yǔ)句 instance = new Singleton(); 后,等待在同步鎖外的其他線程在判斷 if (instance == null) 時(shí),會(huì)重新從主存中讀取 instance 變量,從而發(fā)現(xiàn)其已構(gòu)造完畢,方法實(shí)現(xiàn)了單例模式。