設計模式分為創建型模式、結構型模式和行為型模式。本文講解單例模式,為創建型模式。
特點
單例模式有以下幾個特點:
- 1、單例類只能有一個實例。
- 2、單例類必須自己創建自己的唯一實例。
- 3、單例類必須給所有其他對象提供這一實例。
由以上特點可以知道
單例類中的創建類的語句必須對外界屏蔽。體現出來的特點的就是:構造函數是私有的
創捷單例類的時候,要保證該類僅有一個實例,並提供一個訪問它的全局訪問點
下面我們來看看如何用不同的方式實現以上特點。
實現
首先要強調一點是:按單例類的實例化時機來划分的話,其實實現單例模式就只有兩種模式——懶漢模式與餓漢模式。但由於普通的懶漢模式與餓漢模式都有或多或少一些的缺點。所以衍生出了很多種子模式。接下來我們來了解一下。
一、懶漢模式
懶漢模式是區別於餓漢模式的。不提前做好准備,不到用到的時候不實例化,給人很懶的感覺。所以稱之為懶漢模式。而后面要提到的餓漢,沒用到就已經實例化了,非常飢餓。
public class LazySingleton {
private volatile static LazySingleton INSTANCE; // volatile禁止指令重排
private LazySingleton(){}
public static LazySingleton getInstance(){
if (INSTANCE == null){
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
上面的懶漢模式只能在單線程中使用,也就是說是非線程安全的。從上面普通懶漢模式的代碼可以看出來,在多線程的情況下,實例創建語句可能會被執行多次。所以要實現線程安全的懶漢模式,最簡單的方法就是的加synchronized關鍵字修飾。如下:
可以參考這篇文章,溫習一下synchronized相關知識
public class LazySingleton {
private volatile static LazySingleton INSTANCE; // volatile禁止指令重排
private LazySingleton(){}
public static synchronized LazySingleton getInstance(){
if (INSTANCE == null){
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
以上通過最簡單粗暴的方式,的確可以實現線程安全 的0的,但熟悉的synchronize的同學相信知道該方式性能比較差。后面會介紹的一種效率比較高的實現方式——雙重校驗鎖模式
二、餓漢模式
相比於懶漢模式的“延遲加載”,餓漢模式就是直接在類加載的時候就已經生成唯一的實例了。如以下代代碼實現:
public class HungrySingleton {
private static HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getINSTANCE() {
return INSTANCE;
}
}
三、懶漢模式與餓漢模式的區別
仔細品味懶漢模式與餓漢模式的實現代碼,我們來的看以下兩者的區別:
-
1、懶漢模式,等到要用的時候再創建實例,比較耗時間,但節省空間,典型的時間換空間。
-
2、餓漢模式,不管你用不用得到的,該類的唯一實例都已經在jvm加載該類的時候進行實例化了。
實際上就是在類的連接過程進行內存分配且觸發類的實例化,關於類的生命周期介紹,可以參考另外一篇文章Java類的生命周期淺析
-
3、懶漢模式需要解決線程安全問題。而餓漢模式不需要。因為jvm在加載類的時候是單線程的。可以保證存在單一實例。
四、雙重校驗鎖
雙重校驗鎖是懶漢模式的一種延伸,也即是說對象的實例化是延遲執行的,它是為了解決上面所提到的普通的線程安全懶漢模式效率低下的問題。
該方式,其實同樣為synchronized關鍵字加鎖。但加了兩層校驗,故命名為雙重校驗鎖。
來看下具體的代碼實現
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton INSTANCE;
private DoubleCheckSingleton(){}
public static DoubleCheckSingleton getInstance(){
if (INSTANCE == null){
synchronized (DoubleCheckSingleton.class){
if (INSTANCE == null){
INSTANCE = new DoubleCheckSingleton();
}
}
}
return INSTANCE;
}
}
接下來我們來討論一下該方式相對於synchronized對方法加鎖實現的線程安全的區別在哪。
- 首先我們要知道,我們對實例化過程加鎖的目的,是在於當單例還沒生成時,防止多線程生成多個實例。也就是我們想鎖住的時機,其實是未生成實例的時候。但如果對整個方法進行加鎖,則會導致但你生成實例,以后要每次要獲取實例時,都會受到鎖的影響。這並不是我們想要的。於是乎,我們有必要在加鎖前,先加一層校驗,來防止這個現象發生。
- 至於有人會問,說為何里面還需要多一層檢驗的問題,其實通過的分析可以很容易得出:不加里層校驗,並不能保證單例的結論。因為會存在多個線程都通過第一層校驗的情況,如果不再校驗一次,可能會產生多個實例
總結來說,就是兩次校驗的目的各不一樣,第一層校驗是為了使單例產生后鎖機制失效,避免不必要的開銷。第二層校驗,是為了第保證存在唯一實例和延遲加載。
五、靜態內部類模式
上面我們的已經介紹了懶漢模式與餓漢模式的實現,以及懶漢模式先線程安全的方案實現。從中我們可以看到,懶漢模式下,如果的能解決線程安全,單例的實現時機是比較合理的;而餓漢模式又有創建實例天然的線程安全的優勢,那有沒有一種取兩者精華的實現方式呢?那就是下面要說到的靜態內部類的實現的方式。
關於java內部類的知識,可以參考本篇文章:Java內部類
來看一下代碼實現:
public class StaticInnerClassSingleton {
private static class SingletonInnerClass{
private static StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
private StaticInnerClassSingleton(){}
public static StaticInnerClassSingleton getInstance(){
return SingletonInnerClass.INSTANCE;
}
}
采用靜態內部類實現,能到達一以下兩點:
1、外部類加載時候並不會加載內部類,只會當調用getInstance()
方法的時候才會,也就是的使用的時候才進行加載,這點跟懶漢模式的一致。
2、內部類加載的時候,該單例類的實例化是線程安全的,這點跟餓漢模式相一致。
六、枚舉類
餓漢式以及懶漢式中的雙重檢查式、靜態內部類式都無法避免被反序列化和反射生成多個實例。而枚舉方式實現的單例模式不僅能避免多線程同步的問題,也可以防止反序列化和反射的破壞。
更深入的論證,可參考你知道嗎?枚舉單例模式是世界上最好的單例模式!!!一文。
而關於反序列化破壞單例特性,之前關於序列化的文章Java序列化也有提及,可移步閱讀
public enum EnumSingleton {
INSTANCE;
}
枚舉單例模式具有以下三個優點:
- 寫法簡潔,代碼短小精悍。
- 線程安全。
- 防止反序列化和反射的破壞。