轉載請注明原創出處,謝謝!
HappyFeet的部落格
被 volatile 修飾的變量具備兩種特性:
(1)保證該變量對所有線程的可見性;
(2)禁止指令重排序優化。
1、保證該變量對所有線程的可見性
可見性是指,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主記憶體,同時其他線程每次使用前都會從主記憶體讀取,進而新值對于其他線程來說是可以立即得知的。但普通變量做不到這點,普通變量的值線上程間傳遞均需要通過主記憶體來完成。
在 Java 記憶體模型(深入了解 JVM(6)Java 記憶體模型)中,可以知道,在多線程中,每一個線程都有各自的工作記憶體,工作記憶體是該線程使用到的變量的主記憶體副本拷貝,對變量的所有操作(讀取、指派等)都必須在工作記憶體中進行,并且不會立即更新至主記憶體中。這就可能造成一個變量在一個線程中修改了,還沒來得及更新至主記憶體,主記憶體中該變量的值就被其他線程使用了,而此時變量的值為修改前的舊值。
例如:線程 A 和線程 B 都用到了變量 C,線程 A 中對變量 C 執行了 +1 操作,由于是線上程 A 的工作記憶體中修改的,并且沒有立即回寫至主記憶體,而在此時刻線程 B 從主記憶體讀取變量 C 的值,得到的是過時的結果,即線程 A 對變量 C 的修改對線程 B 來說不可見。
volatile 變量不一樣,它擁有特殊的通路規則:
(1)使用前必須先從主記憶體重新整理最新的值;
(2)修改後必須立刻同步回主記憶體中。
以上兩條特殊規則,給人以
volatile 變量不是存在于工作記憶體而是存在于主記憶體中
的假象,進而保證了 volatile 變量對所有線程的可見性。
雖然 volatile 變量對所有線程是立即可見的,但是,基于 volatile 變量的運算在并發下不一定是安全的。例如:(注意:該例在 IDEA 中隻能以 debug 模式運作,以 run 模式運作會陷入死循環,原因可以參考一下這個:在閱讀《深入了解Java虛拟機》一書中,執行代碼清單12-1的例子時,疑似發現bug?)
public class VolatileTest {
private static final int THREADS_COUNT = 20;
private static volatile int race = 0;
private static void increase() {
race++;
}
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 i = 0; i < 10000; i++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("race = " + race);
}
}
output:
// 這個輸出應該每次都不一樣,都是一個小于 200000 的數字。
race = 189716
如果能夠正确并發的話,最後輸出結果應該是 200000,然後結果卻是小于 200000 的一個數,問題出在哪呢?其實就在 race++ 中,race++ 看似隻有一行代碼,其實是有三個操作:
a)讀取 race 的值;
b)将 race 值加一;
c)将 race 值寫回記憶體。
volatile 關鍵字能夠保證 a)操作讀取的 race 的值在這一時刻是正确的,但在執行 b)、c)操作時,可能有其他的線程已經對 race 的值進行了修改,導緻了 c)操作可能把較小的 race 值同步回主記憶體之中。是以要想保證結果的正确性,需要在 increase() 方法加鎖才行。
以下是适用 volatile 變量的兩種運算場景:
- 運算結果并不依賴變量的目前值,或者能夠保證確定隻有單一的線程修改變量的值。
- 變量不需要與其他的狀态變量共同參與不變限制。
像下面的代碼就很适合使用 volatile 變量來控制并發:
volatile boolean shutdownRequested;
public void shutDown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
但遇到不符合上述兩條規則的運算場景時,就需要加鎖來保證原子性。
2、禁止指令重排序優化
為了盡可能的減小記憶體操作速度遠慢于 CPU 運作速度所帶來的 CPU 空置的影響,虛拟機會按照自己的一些規則将程式編寫順序打亂,而這一打亂,就可能幹擾程式的并發執行。
例如:
Map configOptions;
char[] configText;
// 此變量必須定義為 volatile
volatile boolean initialized = false;
// 假設以下代碼線上程 A 中運作
// 模拟讀取配置資訊, 當讀取完成後将 initialized 設定為 true 以告知其他線程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
// 假設以下代碼線上程 B 中運作
// 等待 initialized 為 true, 代表線程 A 已經把配置資訊初始化完成
while(!initialized) {
sleep();
}
// 使用線程 A 中初始化好的配置資訊
doSomethingWithConfig();
假如上面僞代碼中 initialized 沒有用 volatile 修飾,就可能由于指令重排序的優化,導緻位于線程 A 中最後一句代碼
initialized = true;
被提前執行,這樣線上程 B 中使用配置資訊的代碼就可能出錯。
再比如,double-check locking 中:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
這裡 volatile 修飾變量的作用在于禁止指令重排序,而不是它的可見性。
在 Double-checked_locking 中提到:
Due to the semantics of some programming languages, the code generated by the compiler is allowed to update the shared variable to point to a partially constructed object before A has finished performing the initialization. For example, in Java if a call to a constructor has been inlined then the shared variable may immediately be updated once the storage has been allocated but before the inlined constructor initializes the object.
由于某些程式設計語言的語義,允許編譯器生成的代碼在 A 完成初始化之前更新共享變量以指向部分構造的對象。例如:在 Java 中,如果共享變量的引用已經和構造函數的調用内聯了,即使構造器未完成初始化,共享變量也可能立即被更新。
對應于 double-check locking 例子來說就是:
這一行代碼分三個步驟:
(1)在堆上配置設定記憶體;
(2)賦初值;
(3)将 instance 引用指向堆位址。
假如是以(1)(3)(2)的順序執行,其他線程就可能得到一個未完成初始化的 instance ,導緻程式報錯。而添加 volatile 修飾之後可以阻止指令的重排序(Java 1.5 及以後的版本),進而避免了這種 case。
3、總結
volatile 關鍵字是 Java 虛拟機提供的最輕量級的同步機制,使用 volatile 可能比鎖更快,但在某些情況下它不起作用。在 Java 1.5 中擴充了 volatile 有效的情況範圍。 特别是,雙重檢查加鎖機制現在可以正常工作。
參考資料:
(1)《深入了解 Java 虛拟機》周志明 著.
(2)并發關鍵字volatile(重排序和記憶體屏障)
(3)Double-checked_locking