雙重檢查鎖的由來
在單例模式中,有一個DCL(雙重鎖)的實現方式,在Java程序中,很多時候需要推遲一些高開銷的對象初始化操作,並且只有在使用這些對象的時候才進行開始初始化。
先來看下面實現單例的方式:
非線性安全的延遲初始化對象方式:
public class Test1 {
private static SingletonInstance instance;
private Test1(){}
public static SingletonInstance getInstance(){
if (null == instance){
instance = new SingletonInstance();
}
return instance;
}
}
上面的這種實現方式在高併發的環境下是有問題的,我們可以對getInstance方法做同步處理來實現線性安全,如下:
public class Test1 {
private static SingletonInstance instance;
private Test1() { }
public synchronized static SingletonInstance getInstance(){
if (null == instance){
instance = new SingletonInstance();
}
return instance;
}
}
但是這種同步方式會導致性能的開銷,若getInstance被多個線程頻繁調用,這將會導致程序執行性能的下降。只有在線程調用不多的場景下才可以,性能的開銷可以忽略不計。
基於上述的問題,後來有人提出來了雙重檢查(Double-Checked Locking)的方法,通過雙重檢查來降低同步帶來的性能損耗,如下:
public class DoubleCheckedLockingTest {
private static SingletonInstance instance;
private DoubleCheckedLockingTest() { }
public static SingletonInstance getInstance(){
if (null == instance){
synchronized (DoubleCheckedLockingTest.class) {
if (null == instance) {
instance = new SingletonInstance();
}
}
}
return instance;
}
}
乍一看,是很完美的解決了損耗問題,但是這種做法是錯誤的。
在line7 : instance = new SingletonInstance();創建單例對象的時候可以分解爲下面三行僞代碼:
//1、爲對象分配內存空間
memory = allocation();
//2、初始化對象
initInstance(memory);
//3、設置instance指向剛剛分配的內存空間地址
instance = memory;
在JIT等編譯的時候2-3可能會被重排,如重排後的結果如下:
//1、爲對象分配內存空間
memory = allocation();
//3、設置instance指向剛剛分配的內存空間地址
instance = memory;
//2、初始化對象
initInstance(memory);
因此例如在line4的檢查的時候instance可能還沒有完全初始化好。這也導致了問題的根源所在。
爲了解決重排的問題,我們就可以使用volatile關鍵字,來保證。
public class SalfDoubleCheckedLockingTest {
private volatile static SingletonInstance instance;
private SalfDoubleCheckedLockingTest () { }
public static SingletonInstance getInstance(){
if (null == instance){
synchronized (SalfDoubleCheckedLockingTest.class) {
if (null == instance) {
instance = new SingletonInstance();
}
}
}
return instance;
}
}
另外除了使用volatile關鍵字之外,還可以使用靜態內部類的方式實現線程安全的單例,如下:
public class StaticClassInstance {
private StaticClassInstance(){}
private static class InstanceHandler{
private static SingletonInstance instance = new SingletonInstance();
}
public static SingletonInstance getInstance(){
return InstanceHandler.instance; //在此處會使InstanceHandler類被初始化
}
}
這種方式是基於JVM在類的初始化階段(加載完成後並且未被線程使用之前),會執行類的初始化,在執行類的初始化期間,JVM會去獲取一個鎖,該鎖可以同步多個線程對同一個類的初始化。
其實雙重檢查鎖定(DCL)模式經常會出現在一些框架源碼中,目的是爲了延遲初始化變量。這個模式還可以用來創建單例。下面來看一個 Spring 中雙重檢查鎖定的例子。
public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver {
/** Stores the mappings from namespace URI to NamespaceHandler class name / instance. */
@Nullable
private volatile Map<String, Object> handlerMappings;
/**
* Load the specified NamespaceHandler mappings lazily.
*/
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isTraceEnabled()) {
logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isTraceEnabled()) {
logger.trace("Loaded NamespaceHandler mappings: " + mappings);
}
handlerMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return handlerMappings;
}
}