單例模式的雙重校驗鎖方式如下:
/**
* 雙重校驗鎖(在餓漢模式基礎上進一步優化)
* 1、構造方法私有化
* 2、在定義靜態對象時加volatile鎖來確保初始化時對象的唯一性
* 3、定義獲取對象實例方法,並在方法體中通過synchronized(Object)給單例類加鎖來保障操作的唯一性
* */
class DoubleCheckedLockingSingleton{
private volatile static DoubleCheckedLockingSingleton instance;
private DoubleCheckedLockingSingleton(){}
public static DoubleCheckedLockingSingleton getInstance(){
if(instance == null){
synchronized (DoubleCheckedLockingSingleton.class){
if(instance == null)
instance = new DoubleCheckedLockingSingleton();
}
}
return instance;
}
}
如下,instance爲什麼需要volatile修飾?有什麼作用?解決了什麼問題?
private volatile static DoubleCheckedLockingSingleton instance;
單例模式中使用volatile修飾時可使用雙重校驗鎖方式,至於定義靜態對象時爲什麼需要加volatile來修飾,如代碼註釋所述,volatile確保初始化時對象的唯一性。那麼如果不使用volatile修飾,會出現什麼問題了?
在考慮問題時,先學習下幾個相關知識點,
第一個:volatile是什麼?
Java中提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程,當變量聲明爲volatile後,則編譯時與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其它內存操作一起重排序。
第二個:什麼是競態條件?
在Java多線程中,如果沒有正確的使用同步會出現意想不到的糟糕情況。而競態條件是指在多線程環境下由於不恰當的執行時序而出現不正確的結果。
第三個:什麼是延遲初始化?
延遲初始化的目的是將對象的初始化操作推遲到實際被使用時才執行,同事要確保只能被初始化一次。
第四個:什麼是重排序?
在沒有充分同步的條件下,如果調度器採用不恰當的方式來交替執行不同的線程的操作,將會導致不正確的結果。因此各種操作延遲或者看似亂序執行的不同原因,都可以歸爲重排序。
第五個:什麼是Happens-Before?
JMM爲程序中所有的操作定義了一個偏序關係,即Happens-Before。若想確保線程B正確看到線程A操作的結果,那麼A與B之間必須滿足偏序關係。即確保線程之間表現出串行一致性。
那麼在迴歸到這個問題上,如果不爲instance添加volatile修飾符,則其工作過程是首先檢查是否在沒有同步的情況下需要初始化,如果instance引用不爲空(null),那麼直接使用它即可。否則就進行同步並再次檢查instance是否被初始化,從而保證只有一個線程對共享的instance執行初始化。但真正的問題是,假設線程A先訪問getInstance()時已獲取一個構造好的instance引用,但它並沒有使用同步。而在沒有同步的情況下讀取一個共享對象時,可能看到一個失效值。即在沒有正確同步的情況下發佈一個對象會導致另一個線程看到一個只被部分構造的對象。即刻線程B訪問getInstance()方法,而此時線程B訪問時它可能看到instance仍然爲空。即線程A與線程B之間不存在Happens-Before關係時,且發佈對象時沒有正確使用同步時,線程B並不一定能真正看到instance的正確狀態。
針對單例模式,更傾向於使用延遲初始化佔位類模式,通常被成爲靜態內部類完成單例模式,如下:
/**
* 靜態內部類(類的靜態內部類在JVM中是唯一)
* 1、構造方法私有化
* 2、在類中定義一個靜態內部類,並在內部類中完成對象實例的定義和初始化
* 3、定義獲取對象方法,並通過靜態內部類調用其單例對象
*/
class OuterSingleton{
private static class InnerSingleton{
private static final OuterSingleton INSTANCE = new OuterSingleton();
}
private OuterSingleton(){}
public static final OuterSingleton getInstance(){
return InnerSingleton.INSTANCE;
}
}
爲什麼使用該方式?該方式中使用一個專門的類即InnerSingleton來初始化OuterSingleton。而JVM中將推遲InnerSingleton的初始化操作,直到開始使用這個類時才初始化,並且由於通過一個靜態初始化來初始化INSTANCE,因此不需要額外的同步。當任何一個線程第一次調用getInstance() 時,都會使InnerSingleton被加載和被初始化,此時靜態初始化器將執行OuterSingleton的初始化操作。靜態初始化器是由JVM在類初始化階段執行,即在被類加載後並且被線程使用之前。