1. 概述
關(guān)鍵字volatile可以說時Java虛擬機(jī)提供的最輕量級的同步機(jī)制,但是它并不容易完全被正確、完整的理解,以至于許多程序員都不習(xí)慣去使用它,遇到需要處理并發(fā)問題的時候,一律使用synchronized(synchronized通常稱為重量級鎖)來進(jìn)行同步。
2. 基本概念
Java內(nèi)存模型是圍繞著在并發(fā)過程中如何處理 原子性、可見性 和 有序性 這3個特征來建立的,我們先來看一下這三個特性。
1. 原子性:
由Java內(nèi)存模型來直接保證的原子性變量操作包括read、load、assign、use、store和write,我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的(例外的是long和double的非原子性協(xié)定,讀者只要知道這件事情就可以了)
如果應(yīng)用場景需要一個更大范圍的原子性保證(比如實現(xiàn)一個單例模式的連接等,在實際應(yīng)用中很常見),Java內(nèi)存模型還提供了lock和unlock操作來滿足這種需求,盡管虛擬機(jī)未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱式的使用這兩個操作,這兩個字節(jié)碼指令反映到Java代碼中就是由synchronized關(guān)鍵字標(biāo)識的代碼同步塊,因此在synchronized塊之間的操作也具備原子性。
輔助理解:
int a = 0;
如上是一個簡單的賦值操作(注意:a是非long和double類型的基本類型數(shù)據(jù),double和long類型比較特殊), 這個操作是不可分割的,即,這個操作沒辦法再進(jìn)行拆分,那么我們說這個操作是原子操作。
再比如下面這個操作:
a++;
這個操作實際是a = a + 1;是可分割的,所以這不是一個原子操作。
非原子操作都會存在線程安全問題,需要我們使用同步技術(shù)(如使用sychronized關(guān)鍵字)來讓它變成一個原子操作。
一個操作是原子操作,那么我們稱它具有原子性。
Java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
2. 可見性:
可見性,是指線程之間的可見性,當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。也就是一個線程修改的結(jié)果,另一個線程馬上就能看到。
Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)可見性的。
所以,無論是普通變量還是volatile修飾的變量都是如此,而二者之間的區(qū)別是:volatile的特殊規(guī)則保證了新值能 立即 同步到主內(nèi)存,以及每次使用前 立即 從主內(nèi)存刷新。而普通變量則不能保證這一點(diǎn)。
在 Java 中 volatile、synchronized 和 final 三個關(guān)鍵字可以實現(xiàn)可見性。
可見性是一種復(fù)雜的屬性,因為可見性中的錯誤總是會違背我們的直覺。通常,我們無法確保執(zhí)行讀操作的線程能適時地看到其他線程寫入的值,有時甚至是根本不可能的事情。為了確保多個線程之間對內(nèi)存寫入操作的可見性,必須使用同步機(jī)制。
比如,用volatile修飾的變量會具有可見性。volatile修飾的變量不允許線程內(nèi)部緩存和重排序,即,直接修改內(nèi)存,所以對其他線程是可見的。
但是這里需要注意一個問題:volatile只能讓被它修飾的變量具有可見性,但不能保證它具有原子性。
如下操作:
volatile int a = 0;
……其他操作
a++;
……其他操作
這個變量a具有可見性,但是a++依然是一個非原子操作,也就是這個操作同樣存在線程安全問題。
3. 有序性:
Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。
前半句是指線程內(nèi)表現(xiàn)為串行的語義,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象
Java 語言提供了 volatile 和 synchronized 兩個關(guān)鍵字來保證線程之間操作的有序性,volatile 是因為其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進(jìn)行 lock 操作”這條規(guī)則獲得的,此規(guī)則決定了持有同一個對象鎖的兩個同步塊只能串行執(zhí)行。
3. volatile原理
Java語言提供了一種稍弱的同步機(jī)制,即volatile變量,用來確保將變量的更新操作通知到其他線程。
當(dāng)把變量聲明為volatile類型后,編譯器與運(yùn)行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內(nèi)存操作一起重排序。
volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。
在訪問volatile變量時不會執(zhí)行加鎖操作,因此也就不會使執(zhí)行線程阻塞,因此volatile變量是一種比sychronized關(guān)鍵字更輕量級的同步機(jī)制。

當(dāng)對非 volatile 變量進(jìn)行讀寫的時候,每個線程先從內(nèi)存拷貝變量到CPU緩存中。如果計算機(jī)有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中。
而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內(nèi)存中讀,跳過 CPU cache 這一步。
當(dāng)一個變量定義為 volatile 之后,將具備兩種特性:
- 保證此變量對所有的線程的可見性,這里的“可見性”是指:當(dāng)一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。但普通變量做不到這點(diǎn),普通變量的值在線程間傳遞均需要通過主內(nèi)存來完成。
注意:
在講原子性的時候講到過,volatile修飾的變量并不能保證原子性操作,即,如果被volatile修飾的變量的運(yùn)算并非原子操作,那么在并發(fā)的情況下,該運(yùn)算同樣是不安全的。也即,volatile修飾的變量只能保證可見性和有序性,不一定能保證原子性。
如下代碼,就會出現(xiàn)并發(fā)問題:
package JavaCore;
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
// 可拆解為race = race + 1;,而race + 1也不是原子性的操作
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
increase();
}
}
});
threads[i].start();
}
// 等待1分鐘,足夠運(yùn)算完成了
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(race);
}
}
我們期待的結(jié)果是20000,但最終的結(jié)果往往比這個結(jié)果要小。
所以,在不滿足如下兩條規(guī)則的運(yùn)算場景中,我們?nèi)匀灰ㄟ^加鎖(使用synchronized關(guān)鍵字或使用java.util.concurrent中的原子類)來保證原子性。規(guī)則如下:
a. 運(yùn)算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。
b. 變量不需要與其他的狀態(tài)變量共同參與不變約束。
結(jié)合上面的代碼看,我們可以看出在increase()方法中,race的結(jié)果是依賴當(dāng)前race值的。
下面代碼展示的就是很適合使用volatile變量來控制并發(fā),當(dāng)shutdown()方法被調(diào)用時,能保證所有線程中執(zhí)行的doWork()方法立即停下來。
volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while(!shutdownRequested) {
// do stuff
}
}
- 禁止指令重排序優(yōu)化。有volatile修飾的變量,賦值后多執(zhí)行了一個“l(fā)oad addl $0x0, (%esp)”操作,這個操作相當(dāng)于一個內(nèi)存屏障(指令重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個CPU訪問內(nèi)存時,并不需要內(nèi)存屏障;
什么是指令重排序:是指CPU采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各相應(yīng)電路單元處理。
volatile 性能:
volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為它需要在本地代碼中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行。