設計模式之單例
定義
保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。
簡單來說,也就是需要保證一個類在整個應用程序的生命周期內,只能存在一個實例(沒有也行)。為了達成這個目標,應該要做到以下幾點:
- 私有的構造器,如果是構造器被聲明為public的,則無法控制其實例化;
- 一個獲取實例的公開方法,也就是定義中所說的『提供一個訪問它的全局訪問點』;
- 一個用來保持此唯一實例的靜態變量。
實現方法
首先,我們按照前面說的三點,寫了以下代碼:
public class Singleton {
private Singleton instance;
private Singleton(){}
public Singleton GetInstance(){
return instance;
}
}
這樣,顯然是有問題的。
一是獲取實例的公開方法getInstance,只是生命成public的,外部在沒有Singleton的實例的情況下還是不能調用,就成了一個悖論了。所以需要把GetInstance方法聲明為靜態的。同樣,由于需要被靜態方法調用,同時還要用來保持唯一的實例,instance也需要聲明為靜態的。
二就是還缺少了對instance變量的初始化,即對構造器的調用。我們既可以再聲明instance是就進行初始化,也可以在靜態代碼段中對它進行初始化。
于是乎,就有了下面兩個版本的實現:
方法一:餓漢一
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton GetInstance(){
return instance;
}
}
方法二:餓漢二
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton(){}
public static Singleton GetInstance(){
return instance;
}
}
由于這兩種方法沒有實質上的區別,都是在類被加載的時候就進行了實例的初始化(所以被稱為餓漢式)。這也是餓漢式的下的兩個特點:
- 線程安全,因為在類加載時已經完成來實例化;
- 性能低,實例化后的對象在應用的聲明周期中未必就會被使用,所以可能會產生計算的浪費。
方法三:懶漢
由于餓漢式在類加載時就完成了實例化,導致了可能存在性能浪費,所以我們就考慮看看能不能在類被使用時才被實例化呢。如果聽說過『懶加載』這個的詞話,應該就會覺得這很easy了,在之前的基礎上,很輕松就寫出了下面代碼(懶漢式的單例)。
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton GetInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
乍一看,已經是可以了,至少在單線程下已經沒問題了。但是在多線程的場景下呢,很容易就會產生A、B兩個線程同時調用GetInstance方法,A線程先判斷instance為null,準備進行實例化,但在實例化之前B線程也進行了instance為null的判斷,最終結果是兩個線程分別調用了一次私有構造器進行實例化,第二次實例化的結果會將第一次的覆蓋掉。
所以懶漢式有以下特點:
- 懶加載實現,第一次調用時實例化,不調用則不實例化,故計算效率高;
- 線程不安全,上面已經詳細描述來是如何產生線程沖突的。
方法四:懶漢(線程安全)
為了解決上面方法的缺陷,也就是所謂的線程不安全,我們找到了synchronized這個關鍵字,也只加了這個關鍵字,得到下面的代碼。
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton GetInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
與前一種方法相比,在實現上只是在getInstance方法上增加了synchronized關鍵字,使得GetInstance方法同步,同一時間只能有一個線程執行這個方法,那我們之前說的線程不安全的問題顯然就不在了,這樣是不是就完美來呢?世界沒那么美好,我們又引入了新的問題。
由于是在整個GetInstance方法上加鎖(同步),但是因為實際上只需要進行一次實例化(也只允許進行一次),所以絕大多數場景下是不需要同步的,所以在并發場景下會導致效率降低,相當于多車道在這里并成單車道了。
方法五:懶漢(雙重檢查鎖定)
既然還有問題,那我們就來繼續進行優化。上面的方法因為把整個GetInstance方法設置為synchronized,所以導致多線程在這里受阻,那我們把同步的范圍縮小一點兒,看看情況會不會好一些。
public class LazySingleton {
private static volatile LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized(LazySingleton.class){
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
優化之后,把鎖定范圍進行了收縮,只在需要進行初始化實例時才進行同步,之后就不再進行同步。
這樣我們就得到了一種效率較高,并且線程安全的單例模式的構造方法。
PS. 如果有仔細看代碼,您或許會發現我們在聲明instance變量的時候,用了一個volatile關鍵字,如果需要一些解釋的話,可以參考Java中的volatile在使用雙層檢查實現單例模式的解讀,后面我也可能來單獨說一下這個。
第六種:靜態內部類
還有一種使用靜態內部類來實現的單例,也被各種推薦。因為內部靜態類是要在有引用了以后才會裝載到內存的,這樣就同樣實現了懶加載;同時,靜態內部類的靜態變量的初始化,也是在被加載時進行的初始化,天然的完成來對進程安全的控制。
public class InnerStaticClassSingleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static InnerStaticClassSingleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
To Be Continued
<a rel="license" >
</a><br />本作品采用<a rel="license" >知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議</a>進行許可。