定義: 保證一個類僅有一個實例,並提供一個全局訪問點 適用場景: 確保任何情況下這個對象只有一個實例 詳解: 1.私有構造器: 將本類的構造器私有化,其實這是單例的一個非常重要的步驟,沒有這個步驟,可以說你的就不是單例模式。這個步驟其實是防止外部函數在new的時候能構造出來新的對象,我們說單例要保證 ...
定義:
保證一個類僅有一個實例,並提供一個全局訪問點
適用場景:
確保任何情況下這個對象只有一個實例
詳解:
- 私有構造器
- 單利模式中的線程安全+延時載入
- 序列化和反序列化安全,
- 防止反射攻擊
- 結合JDK源碼分析設計模式
1.私有構造器:
將本類的構造器私有化,其實這是單例的一個非常重要的步驟,沒有這個步驟,可以說你的就不是單例模式。這個步驟其實是防止外部函數在new的時候能構造出來新的對象,我們說單例要保證一個類只有一個實例,如果外部能new新的對象,那我們單例就是失敗的。所以無論什麼時候一定要將這個構造器私有化
2.單例模式中的線程安全+延時載入(懶漢式):
其實從單線程角度來看,懶漢式是安全。這裡我們先來介紹一個線程安全的懶漢式接下來我們從三個版本的懶漢式來分析如何即做到線程安全又做到效率提高
2.1原始版本
public class LazySingleton { private static LazySingleton lazySingleton = null; private LazySingleton(){ if(lazySingleton != null){ throw new RuntimeException("單例構造器禁止反射調用"); } } public static LazySingleton getInstance(){ if(lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; }
我們來稍微分析一下為什麼線程不安全,現在有A,B兩個線程,假設兩個線程同時都走到了lazySingleton = new LazySingleton();這個創建對象的行,當都執行完的時候,就會創建兩個不同的對象然後分別返回。所以違背了單例模式的定義
2.2加鎖
可能很多人會直接在getInstance()方法上加一個synchronize關鍵字,這樣做完全可以但是效率會較慢,因為synchronize相當於鎖了整個對象,下麵的雙鎖結構就會比較輕量級一點
public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; private LazyDoubleCheckSingleton(){ } public static LazyDoubleCheckSingleton getInstance(){ if(lazyDoubleCheckSingleton == null){ synchronized (LazyDoubleCheckSingleton.class){ if(lazyDoubleCheckSingleton == null){ lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); } } } return lazyDoubleCheckSingleton; } }
可能很多人一眼就看見synchronize關鍵字位置變換了,鎖的範圍變小了,但是最關鍵的一個是private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;中的volatile關鍵字,因為如果不加這個關鍵字的時候,JVM會對沒有依賴關係的語句進行重排序,就是可能會線上程A的時候底層先設置lazyDoubleCheckSingleton 指向剛分配的記憶體地址,然後再來初始化對象,線程B呢線上程A設置lazyDoubleCheckSingleton 指向剛分配的記憶體地址完後就走到了第一個if,這時判斷是不為空的所以都沒有競爭synchronize中的資源就直接返回了,但是註意線程A並沒有初始化完對象,所以這時就會出錯。為瞭解決上述問題,我們可以引入volatile關鍵字,這個關鍵字是會有讀屏障寫屏障的,也就是由這個關鍵字修飾的變數,它中間的操作會額外加一層屏障來隔絕,詳情可以參考這篇博客。就會禁止一些操作的重排序。
2.3靜態內部類
public class StaticInnerClassSingleton { private static class InnerClass{ private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton getInstance(){ return InnerClass.staticInnerClassSingleton; } private StaticInnerClassSingleton(){ if(InnerClass.staticInnerClassSingleton != null){ throw new RuntimeException("單例構造器禁止反射調用"); } } }
我在類內部直接定義一個靜態內部類,在這個類需要載入的時候我直接把初始化的工作放在了靜態內部類中,當有幾個線程進來的時候,在class載入後被線程使用之前都是類的初始化階段,在這個階段JVM會獲取一個鎖,這個鎖可以同步多個線程對一個類的初始化,然後在內部類的初始化中會進行StaticInnerClassSingleton類的初始化。可以這麼理解,其實我們這個也是加了鎖,不過這是JVM內部加的鎖。
3.序列化與反序列化安全
下麵先介紹一下餓漢式
public class HungrySingleton implements Serializable{ private final static HungrySingleton hungrySingleton; static{ hungrySingleton = new HungrySingleton(); } private HungrySingleton(){ if(hungrySingleton != null){ throw new RuntimeException("單例構造器禁止反射調用"); } } public static HungrySingleton getInstance(){ return hungrySingleton; } private Object readResolve(){ return hungrySingleton; } }
餓漢式就是在類的初始化階段就已經載入好了,就算你不用這個對象,這個對象也已經創建好,不像懶漢式要等到要用的時候才載入。這是兩種模式的一個很大的區別,事實上餓漢式是線程安全的,就像懶漢式的內部類載入一樣,是由JVM加的鎖,但是兩者都不一定是序列化安全的。
上面的餓漢式是序列化安全的,為什麼?因為多加了readResolve()方法。這時候有人會問為什麼要在餓漢式上多加一個這個方法。這裡的源碼我就不一一解析了。事實上在反序列化(從文件中讀取類)的時候,底層會有一個判斷。如果這個類在運行時是可序列化的,那麼我就會在讀取的時候創建一個新的類(反射創建),否則我就會讓這個類為空。再後面又有一個判斷,如果我的類這時候不為空,我就會通過反射嘗試調用readResolve()方法,然後最終返回給我的ObjectInputStream流。沒有的話我就返回之前創建的新對象。所以這就相當於覆蓋了之前讀取時候創建的類
4.防止反射攻擊
看完上面的代碼你會發現,我基本上都在私有構造器中加入一個空判斷來拋出異常,反射攻擊的時候,上面的懶漢式中的內部類代碼和餓漢式中的序列化安全代碼都是可以防禦發射攻擊的,當然會拋出相應異常,接下來我們介紹一下枚舉單例模式
public enum EnumInstance { INSTANCE; private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumInstance getInstance(){ return INSTANCE; } }
枚舉對象不能被反射創建,並且序列化與反序列化中枚舉類型不會被創建出新的,下麵看看枚舉類型的構造器
protected Enum(String name,int ordinal){ this.name=name; this.ordinal=ordinal; }
可見這個構造器是有參的,並且由這兩個值確定了枚舉唯一性,不會由序列化與反序列化破壞。並且也是線程安全的,原理同內部類。所以非常推薦枚舉類型來完成單例模式。
5.源碼解析:
JDK中Runtime類就是一個單例模式,它不准外部創建實例,構造器代碼如下:
/** Don't let anyone else instantiate this class */ private Runtime() {}
並且還是餓漢式,代碼如下:
private static Runtime currentRuntime = new Runtime(); public static Runtime getRuntime() { return currentRuntime; }
相信理解了上面的模式,可以很容易的明白這個類的設計模式
當然還有我們常用的Spring框架,簡單說一下就是Spring中對象創建在Bean作用域中僅創建一個,和我們上面講的單例還是有稍許區別,這個單例的作用域是整個應用的上下文,通俗一點理解就是Spring就像一個商店,裡面的商品一種只有一個,大家看見的一個商品都是同一個,這一種商品中不會再有另一個商品了。