在Java併發編程中,synchronized和volatile 都扮演着重要的角色,volatile是輕量級的synchronzied,其在多處理器開發時保證了共享變量的"可見性".
問題引入:多個CPU的不可見性造成髒讀
我們知道CPU速度非常快,比內存快百倍以上,所以CPU更希望和速度相近的CPU cache打交道。
而一個多核的CPU本質上就是多個CPU共用一個外殼,每個核就是一個單核CPU,其都有屬於自己的cache。
當更改一個變量時,CPU將值寫入到對應的cache中,不一定寫入內存中,這個CPU認爲值已經改變了,但是其他CPU認爲值沒有改變。這樣就會出現髒讀。
解決方式:volatile 指令實現可見性
volatile 底層是使用Lock指令。
CPU讀取到lock指令後,會做以下操作。
1. 將對應的值先到CPU對應的cache中
2. 將對應的值寫入到內存中(就是內存條,我們稱其爲系統內存)
3. 第二部的寫操作會使在其他CPU裏緩存了該地址的數據無效。
4. 其他CPU當需要這個數值時,必須去重新讀取內存。
這樣就保證了多CPU之間的可見性。
經典案例:基於雙重檢查的單例模式
單例模式就是保證一個類始終只有一個對象的一種編碼方法。我們可以很容易的寫出來一個單線程下的單例餓漢模式
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
// 可能同時有多個線程進入if中
instance = new Singleton();
}
return instance;
}
}
在多線程時,可以會new出多個實例.
我們可以通過synchronized關鍵子鎖住這個方法,保證其線程安全。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
synchronized是把這個方法鎖成一個單線程方法,效率降低十分明顯。
我們通過volatile進行優化,將其synchronized鎖的粒度減小。
public class Singleton {
// 必須使用 volatile 修飾,防止因爲多CPU之間的不可見性而出錯。
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}