本文對(duì)java中volatile關(guān)鍵字的作用及使用進(jìn)行了詳細(xì)介紹。
本文首發(fā):http://yuweiguocn.github.io/
《春曉》
春眠不覺曉,處處聞啼鳥。
夜來風(fēng)雨聲,花落知多少?
—唐,孟浩然
作用
volatile作用主要有兩個(gè),一是保證多線程環(huán)境下共享變量的可見性,二是禁止指令重排序。
緩存一致性
首先從計(jì)算機(jī)的內(nèi)存模型和Java內(nèi)存模型來分析下多線程環(huán)境下普通共享變量的可見性問題。
計(jì)算機(jī)內(nèi)存模型
通常稱被多個(gè)線程訪問的變量稱為 共享變量。可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
我們運(yùn)行的程序會(huì)被編譯成指令放到CPU中運(yùn)行,程序在運(yùn)行過程中的臨時(shí)數(shù)據(jù)放在主存(物理內(nèi)存)中,由于CPU的運(yùn)算速度很快,但從主存數(shù)據(jù)讀取和寫入速度很慢,所以為了提高CPU運(yùn)算效率,在CPU中開辟了一塊存儲(chǔ)稱之為高速緩存。程序在運(yùn)行過程中,會(huì)將需要的數(shù)據(jù)復(fù)制一份到高速緩存,這樣CPU在執(zhí)行指令時(shí)會(huì)從高速緩存中讀取和寫入數(shù)據(jù),運(yùn)算過程結(jié)束會(huì)將數(shù)據(jù)從高速緩存中寫回到主存。
高速緩存在單線程中是沒問題的,但在多線程中可能會(huì)出現(xiàn)問題。在多核CPU 中,每條線程可能運(yùn)行于不同的 CPU 中,因此 每個(gè)線程運(yùn)行時(shí)有自己的高速緩存(對(duì)單核CPU來說,其實(shí)也會(huì)出現(xiàn)這種問題,只不過是以線程調(diào)度的形式來分別執(zhí)行的)。舉個(gè)例子,下面的代碼由兩個(gè)線程運(yùn)行,i 的初始值為0:
i = i + 1;
兩個(gè)線程分別讀取 i 的值到各自所在CPU的高速緩存中,線程1執(zhí)行加1操作后 i 的值為1,然后將 i 的值寫入到主存中,然后線程2執(zhí)行加1操作,由于線程2的高速緩存中 i 的值為0,所以執(zhí)行加1操作后 i 的值為1,線程2將 i 的值寫入到主存中,最終 i 的值是1而不是2,這就是著名的緩存一致性問題 。為了解決緩存不一致的問題,在硬件層面通常有兩種解決方案:一是緩存一致性協(xié)議,二是通過在總線加Lock#鎖的方式。這里我們不再深入介紹。

Java內(nèi)存模型
Java內(nèi)存模型規(guī)定所有的變量都是存在主存當(dāng)中(類似于前面說的物理內(nèi)存),每個(gè)線程都有自己的工作內(nèi)存(類似于前面的高速緩存)。線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接對(duì)主存進(jìn)行操作。并且每個(gè)線程不能訪問其他線程的工作內(nèi)存。所以在Java內(nèi)存模型中同樣存在一致性問題。

使用 volatile 保證可見性
對(duì)于可見性,Java提供了 volatile 關(guān)鍵字來保證可見性。當(dāng)一個(gè)共享變量被 volatile 修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。而普通的共享變量不能保證可見性,因?yàn)槠胀ü蚕碜兞勘恍薷闹螅裁磿r(shí)候被寫入主存是不確定的,當(dāng)其他線程去讀取時(shí),此時(shí)內(nèi)存中可能還是原來的舊值,因此無法保證可見性。
來看一段代碼,假如線程1先執(zhí)行,線程2后執(zhí)行:
//線程1
boolean stop = false;
while(!stop){
doSomething();
}
//線程2
stop = true;
這段代碼是很典型的一段代碼,很多人在中斷線程時(shí)可能都會(huì)采用這種標(biāo)記辦法。但是事實(shí)上,這段代碼會(huì)完全運(yùn)行正確么?即一定會(huì)將線程中斷么?不一定,也許在大多數(shù)時(shí)候,這個(gè)代碼能夠把線程中斷,但是也有可能會(huì)導(dǎo)致無法中斷線程(雖然這個(gè)可能性很小,但是只要一旦發(fā)生這種情況就會(huì)造成死循環(huán)了)。
下面解釋一下這段代碼為何有可能導(dǎo)致無法中斷線程。在前面已經(jīng)解釋過,每個(gè)線程在運(yùn)行過程中都有自己的工作內(nèi)存,那么 線程1 在運(yùn)行的時(shí)候,會(huì)將 stop 變量的值拷貝一份放在自己的工作內(nèi)存當(dāng)中。
那么當(dāng) 線程2 更改了 stop 變量的值之后,但是還沒來得及寫入主存當(dāng)中, 線程2 轉(zhuǎn)去做其他事情了,那么 線程1 由于不知道 線程2 對(duì) stop 變量的更改,因此還會(huì)一直循環(huán)下去。但是用 volatile 修飾之后就變得不一樣了:
- 使用 volatile 關(guān)鍵字會(huì)強(qiáng)制將修改的值立即寫入主存;
- 使用 volatile 關(guān)鍵字的話,當(dāng) 線程2 進(jìn)行修改時(shí),會(huì)導(dǎo)致 線程1 的工作內(nèi)存中緩存變量 stop 的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對(duì)應(yīng)的緩存行無效);
- 由于 線程1 的工作內(nèi)存中緩存變量 stop 的緩存行無效,所以 線程1 再次讀取變量 stop 的值時(shí)會(huì)去主存讀取。
- 那么在 線程2 修改 stop 值時(shí)(當(dāng)然這里包括2個(gè)操作,修改線程2工作內(nèi)存中的值,然后將修改后的值寫入內(nèi)存),會(huì)使得 線程1 的工作內(nèi)存中緩存變量 stop 的緩存行無效,然后 線程1 讀取時(shí),發(fā)現(xiàn)自己的緩存行無效,它會(huì)等待緩存行對(duì)應(yīng)的主存地址被更新之后,然后去對(duì)應(yīng)的主存讀取最新的值。那么線程1讀取到的就是最新的正確的值。
指令重排序
指令重排序,一般來說,處理器為了提高程序運(yùn)行效率,可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
volatile 關(guān)鍵字禁止指令重排序有兩層意思:
- 當(dāng)程序執(zhí)行到 volatile 變量的讀操作或者寫操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見,在其后面的操作肯定還沒有進(jìn)行;
- 在進(jìn)行指令優(yōu)化時(shí),不能將在對(duì) volatile 變量訪問的語句放在其后面執(zhí)行,也不能把 volatile 變量后面的語句放到其前面執(zhí)行。
對(duì)于使用雙重檢查鎖定實(shí)現(xiàn)單例的方式,單例的引用會(huì)聲明為volatile,這里的volatile有什么作用?
public class Singleton {
private Singleton(){}
private volatile static Singleton instance;
public static Singleton getInstance() {
Singleton tempInstance=instance;
if (tempInstance == null) {
synchronized (Singleton.class) {
tempInstance=instance;
if (tempInstance == null) {
tempInstance = new Singleton();
instance = tempInstance;
}
}
}
return tempInstance;
}
}
instance = new Singleton();通過這一行代碼創(chuàng)建一個(gè)對(duì)象,可以分解為如下的三行偽代碼:
memory = allocate(); //1:分配對(duì)象的內(nèi)存空間
ctorInstance(memory); //2:初始化對(duì)象
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
上面三行偽代碼中的2和3之間,可能會(huì)被重排序(在一些JIT編譯器上,這種重排序是真實(shí)發(fā)生的)2和3之間重排序之后的執(zhí)行時(shí)序如下:
memory = allocate(); //1:分配對(duì)象的內(nèi)存空間
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
//注意,此時(shí)對(duì)象還沒有被初始化!
ctorInstance(memory); //2:初始化對(duì)象
如果發(fā)生重排序,另一個(gè)并發(fā)執(zhí)行的線程B就有可能在第一次為空判斷instance時(shí)不為null。線程B接下來將訪問instance所引用的對(duì)象,但此時(shí)這個(gè)對(duì)象可能還沒有被A線程初始化!
當(dāng)聲明對(duì)象的引用為volatile后,上面的三行偽代碼中的2和3之間的重排序,在多線程環(huán)境中將會(huì)被禁止。
volatile 的原理和實(shí)現(xiàn)機(jī)制
前面講述了源于volatile關(guān)鍵字的一些使用,下面我們來探討一volatile到底如何保證可見性和禁止指令重排序的。下面這段話摘自《深入理解Java虛擬機(jī)》:
觀察加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn),加入volatile關(guān)鍵字時(shí),會(huì)多出一個(gè)lock前綴指令
lock前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障(也成內(nèi)存柵欄),內(nèi)存屏障會(huì)提供3個(gè)功能:
- 它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成;
- 它會(huì)強(qiáng)制將對(duì)緩存的修改操作立即寫入主存;
- 如果是寫操作,它會(huì)導(dǎo)致其他CPU中對(duì)應(yīng)的緩存行無效。