自從jdk1.5以後,
volatile
可謂發生了翻天覆地的變化,從一個一直被吐槽的關鍵詞,變成一個輕量級的線程通信代名詞。
接下來我們將從以下幾個方面來分析以下volatile
。
-
重排序與
as if serial
的關係 -
volatile
的特點 -
volatile
的內存語義 -
volatile
的使用場景
重排序與as if serial的關係
重排序值得是編譯器與處理器爲了優化程序的性能,而對指令序列進行重新排序的。
但是並不是什麼情況下都可以重排序的,
-
數據依賴
a = 1; // 1 b = 2; // 2
在這種情況,1、2不存在數據依賴,是可以重排序的。
a = 1; // 1 b = a; // 2
在這種情況,1、2存在數據依賴,是禁止重排序的。
-
as if serial
簡單的理解就是。不管怎麼重排序,在單線程情況下程序的執行結果是一致。
根據 as if serial
原則,它強調了單線程。那麼多線程發生重排序又是怎麼樣的呢?
請看下面代碼
public class VolatileExample1 {
/**
* 共享變量 name
*/
private static String name = "init";
/**
* 共享變量 flag
*/
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
name = "yukong"; // 1
flag = true; // 2
});
Thread threadB = new Thread(() -> {
if (flag) { // 3
System.out.println("flag = " + flag + " name = " +name); // 4
};
});
}
}
上面代碼中,name輸出一定是yukong
嗎,答案是不一定,根據happen-before
原則與as if serial
原則,由於 1、2不存在依賴關係,可以重排序,操作3、操作4也不存在數據依賴,也可以重排序。
那麼就有可能發生下面的情況
1535967197003.png
上圖中,操作1與操作2發生了重排序,程序運行的時候,線程A先將flag更改成true,然後線程B讀取flag變量並且判斷,由於此時flag已經是true,線程B將繼續讀取name的值,由於此時線程name的值還沒有被線程A寫入,那麼線程此時輸出的name就是初始值,因爲在多線程的情況下,重排序存在線程安全問題。
volatile的特點
volatile
變量具有以下的特點。
- 可見性。對於一個
volatile
變量的讀,總是能看到任意線程對這個變量的最後的修改。 - 有序性。對於存在指令重排序的情況,
volatile
會禁止部分指令重排序。
這裏我先介紹一下volatile
關鍵詞的特點,接下來我們將會從它的內存語義來解釋,爲什麼它會具有以上的特點,以及它使用的場景。
volatile的內存語義
- 當寫一個
volatile
變量時,JMM會立即將本地變量中對應的共享變量值刷新到主內存中。 - 當讀一個
volatile
變量時,JMM會將線程本地變量存儲的值,置爲無效值,線程接下來將從主內存中讀取共享變量。
如果一個場景存在對volatile
變量的讀寫場景,在讀線程B讀一個volatile
變量後,,寫線程A在寫這個volatile
變量前所有的所見的共享變量的值都將會立即變得對讀線程B可見。
那麼這種內存語義是怎麼實現的呢?
其實編譯器生產字節碼的時候,會在指令序列中插入內存屏障來禁止指令排序。下面就是JMM內存屏障插入的策略。
- 在每一個volatile寫操作前插入一個StoreStore屏障
- 在每一個volatile寫操作後插入一個StoreLoad屏障
- 在每一個volatile寫操作後插入一個LoadLoad屏障
- 在每一個volatile讀操作後插入一個LoadStore屏障
那麼這些策略中,插入這些屏障有什麼作用呢?我們逐條逐條分析一下。
- 在每一個volatile寫操作前插入一個StoreStore屏障,這條策略保證了volatile寫變量與之前的普通變量寫不會重排序,即是隻有當volatile變量之前的普通變量寫完,volatile變量纔會寫。 這樣就保證volatile變量寫不會跟它之前的普通變量寫重排序
- 在每一個volatile寫操作後插入一個StoreLoad屏障,這條策略保證了volatile寫變量與之後的volatile寫/讀不會重排序,即是隻有當volatile變量寫完之後,你後面的volatile讀寫才能操作。 這樣就保證volatile變量寫不會跟它之後的普通變量讀重排序
- 在每一個volatile讀操作後插入一個LoadLoad屏障,這條策略保證了volatile讀變量與之後的普通讀不會重排序,即只有當前volatile變量讀完,之後的普通讀才能讀。 這樣就保證volatile變量讀不會跟它之後的普通變量讀重排序
- 在每一個volatile讀操作後插入一個LoadStore屏障,這條策略保證了volatile讀變量與之後的普通寫不會重排序,即只有當前volatile變量讀完,之後的普通寫才能寫。樣就保證volatile變量讀不會跟它之後的普通變量寫重排序
根據這些策略,volatile變量禁止了部分的重排序,這樣也是爲什麼我們會說volatile具有一定的有序的原因。
根據以上分析的volatile
的內存語義,大家也就知道了爲什麼前面我們提到的happen-before
原則會有一條
- volatile的寫happen-before與volaile的讀。
那麼根據volatile
的內存語義,我們只需要更改之前的部分代碼,只能讓它正確的執行。
即把flag定義成一個volatile
變量即可。
public class VolatileExample1 {
/**
* 共享變量 name
*/
private static String name = "init";
/**
* 共享變量 flag
*/
private volatile static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
name = "yukong"; // 1
flag = true; // 2
});
Thread threadB = new Thread(() -> {
if (flag) { // 3
System.out.println("flag = " + flag + " name = " +name); // 4
};
});
}
}
我們來分析一下
-
由於 flag是volatile變量 那麼在volatile寫之前插入一個storestore內存屏障,所以1,2不會發生重排序,即1happen before 2
-
由於 flag是volatile變量 那麼在volatile讀之後插入一個loadload內存屏障,所以3,4不會發生重排序,即3happen before 4
-
根據happen-before原則,volatile寫happen before volatile讀,即是 2happen before 3。
-
根據happen-before的傳遞性,所以1 happen before4。
1535969126569.png
volatile的使用場景
- 標記變量,也就是上面的flag使用
- double check 單例模式中
下面我們看看double check的使用
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;
}
}
至於爲何需要這麼寫請參考:
《Java 中的雙重檢查(Double-Check)》http://www.iteye.com/topic/652440
喜歡的小夥伴可以點下關注,後續還會有相關服務器搭建系列文章繼續更新!!!