這是 wanAndroid 每日一問中的一道題,下面我們來嘗試解答一下。
講講并發(fā)專題 volatile,synchronized,CAS,happens before, lost wake up
為了本系列的「短平快」,今天我們就來第一個(gè)主角:volatile。
保證內(nèi)存可見性
前面我們講到:Java 內(nèi)存模型分為了主內(nèi)存和工作內(nèi)存兩部分,其規(guī)定程序所有的變量都存儲(chǔ)在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程使用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作(賦值、讀取等)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀取主內(nèi)存中的變量。不同線程之間也無法直接訪問對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞都必須經(jīng)過主內(nèi)存的傳遞來完成。

這樣就會(huì)存在一個(gè)情況,工作內(nèi)存值改變后到主內(nèi)存更新一定是需要一定時(shí)間的,所以可能會(huì)出現(xiàn)多個(gè)線程操作同一個(gè)變量的時(shí)候出現(xiàn)取到的值還是未更新前的值。
這樣的情況我們通常稱之為「可見性」,而我們加上 volatile 關(guān)鍵字修飾的變量就可以保證對(duì)所有線程的可見性。
這里的可見性是什么意思呢?當(dāng)一個(gè)線程修改了變量的值,新的值會(huì)立刻同步到主內(nèi)存當(dāng)中。而其他線程讀取這個(gè)變量的時(shí)候,也會(huì)從主內(nèi)存中拉取最新的變量值。
為什么 volatile 關(guān)鍵字可以有這樣的特性?這得益于 Java 語言的先行發(fā)生原則(happens-before)。簡(jiǎn)單地說,就是先執(zhí)行的事件就應(yīng)該先得到結(jié)果。
但是! volatile 并不能保證并發(fā)下的安全。
Java 里面的運(yùn)算并非原子操作,比如 i++ 這樣的代碼,實(shí)際上,它包含了 3 個(gè)獨(dú)立的操作:讀取 i 的值,將值加 1,然后將計(jì)算結(jié)果返回給 i。這是一個(gè)「讀取-修改-寫入」的操作序列,并且其結(jié)果狀態(tài)依賴于之前的狀態(tài),所以在多線程環(huán)境下存在問題。
要解決自增操作在多線程下線程不安全的問題,可以選擇使用 Java 提供的原子類,如
AtomicInteger或者使用synchronized同步方法。原子性:在 Java 中,對(duì)基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執(zhí)行,要么不執(zhí)行。也就是說,只有簡(jiǎn)單的讀取、賦值(而且必須是將數(shù)字賦值給某個(gè)變量)才是原子操作。(變量之間的相互賦值不是原子操作,比如
y = x,實(shí)際上是先讀取x的值,再把讀取到的值賦值給y寫入工作內(nèi)存)
禁止指令重排
最開始看到「指令重排」這個(gè)詞語的時(shí)候,我也是一臉懵逼。后面看了相關(guān)書籍才知道,處理器為了提高程序效率,可能對(duì)輸入代碼進(jìn)行優(yōu)化,它不保證各個(gè)語句的執(zhí)行順序同代碼中的順序一致,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
指令重排是一把雙刃劍,雖然優(yōu)化了程序的執(zhí)行效率,但是在某些情況下,卻會(huì)影響到多線程的執(zhí)行結(jié)果。比如下面的代碼:
boolean contextReady = false;
//在線程A中執(zhí)行:
context = loadContext(); // 步驟 1
contextReady = true; // 步驟 2
//在線程B中執(zhí)行:
while(!contextReady ){
sleep(200);
}
doAfterContextReady (context);
以上程序看似沒有問題。線程 B 循環(huán)等待上下文 context 的加載,一旦 context 加載完成,contextReady == true 的時(shí)候,才執(zhí)行 doAfterContextReady 方法。
但是,如果線程 A 執(zhí)行的代碼發(fā)生了指令重排,也就是上面的步驟 1 和步驟 2 調(diào)換了順序,那線程 B 就會(huì)直接跳出循環(huán),直接執(zhí)行 doAfterContextReady() 方法導(dǎo)致出錯(cuò)。
而 volatile 采用「內(nèi)存屏障」這樣的 CPU 指令就解決這個(gè)問題,不讓它指令重排。
使用場(chǎng)景
從上面的總結(jié)來看,我們非常容易得出 volatile 的使用場(chǎng)景:
- 運(yùn)行結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。
- 變量不需要與其他的狀態(tài)變量共同參與不變約束。
比如下面的場(chǎng)景,就很適合使用 volatile 來控制并發(fā),當(dāng) shutdown() 方法調(diào)用的時(shí)候,就能保證所有線程中執(zhí)行的 work() 立即停下來。
volatile boolean shutdownRequest;
private void shutdown(){
shutdownRequest = true;
}
private void work(){
while (!shutdownRequest){
// do something
}
}
總結(jié)
說了這么多,其實(shí)對(duì)于 volatile 我們只需要知道,它主要特性:保證可見性、禁止指令重排、解決 long 和 double 的 8 字節(jié)賦值問題。
還有一個(gè)比較重要的是:它并不能保證并發(fā)安全,不要和 synchronized 混淆。
細(xì)心的你還會(huì)發(fā)現(xiàn),在 Kotlin 語言中,其實(shí)是沒有
volatile和synchronized這樣的關(guān)鍵字的,那 Kotlin 是怎么處理并發(fā)問題的呢?感興趣的一定要去看看。
文章參考:
漫畫:什么是volatile關(guān)鍵字?(整合版)
《深入理解 Java 虛擬機(jī)》