設計模式—單例模式(Singleton)
一、什麼是單例模式:
單例(Singleton)模式是一種常用的建立型設計模式。
簡單來說就是一個類只能構建一個物件的設計模式。
核心作用:保證一個類只有一個例項,並且提供一個訪問該例項的全域性訪問點。
二、單例模式的應用場景:
1、需要生成唯一序列的環境
2、需要頻繁例項化然後銷燬的物件。
3、建立物件時耗時過多或者耗資源過多,但又經常用到的物件。
4、方便資源相互通訊的環境
舉個例子:
1、windows桌面上的回收站,當我們試圖再次開啟一個新的回收站時,Windows系統並不會為你彈出一個新的回收站視窗。
也就是說整個windows系統執行過程中只會維護一個回收站例項。
2、一般網站上統計實時線上人數的計數器也是單例模式。
三、單例模式的優缺點:
優點:
1、在記憶體裡只有一個例項,減少了記憶體的開銷,尤其是頻繁的建立和銷燬例項(比如管理學院首頁頁面快取)。
2、避免對資源的多重佔用(比如寫檔案操作)。
缺點:
1、不適用於變化的物件,如果同一型別的物件總是要在不同的用例場景發生變化,單例就會引起資料的錯誤,不能儲存彼此的狀態。
2、由於單利模式中沒有抽象層,因此單例類的擴充套件有很大的困難。
3、單例類的職責過重,在一定程度上違背了“單一職責原則”。
四、單例模式的實現:
單例模式大致的實現步驟:
1、私有建構函式,防止被例項化
2、持有私有靜態例項
3、公開靜態工廠方法,獲取唯一可用的物件
單例模式的幾種實現方式:
1、餓漢式
它是在類裝載時例項化物件,所以不支援懶載入,但它是執行緒安全的,也是平常使用較多的一種方式。
/** 單例模式-餓漢式-執行緒安全 */ public class Singleton { // 私有建構函式,防止被例項化 private Singleton(){} // 單例物件 類載入時建立instance 避免了多執行緒同步問題 private static Singleton instance = new Singleton(); // 靜態工廠方法,獲取唯一可用的物件 public static Singleton getInstance() { return instance; } }
2、懶漢式
需要用時例項化物件,支援懶載入,非執行緒安全。因為沒有加鎖 synchronized,所以嚴格意義上它並不算單例模式。
/** 單例模式-懶漢式-非執行緒安全 */ public class Singleton { // 私有建構函式,防止被例項化 private Singleton(){} // 單例物件 此處賦值為null,目的是實現延遲載入 private static Singleton instance = null; // 靜態工廠方法,建立唯一可用的物件 public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
那麼如何讓他支援多執行緒,變成執行緒安全呢? 加鎖 synchronized
/** 單例模式-懶漢模式-執行緒安全 */ public class Singleton { // 私有建構函式,防止被例項化 private Singleton(){}; // 單例物件 此處賦值為null,目的是實現延遲載入 private static Singleton instance = null; // 靜態工廠方法,建立唯一可用的物件 public static synchronized Singleton getInstance(){ if (instance == null) { instance = new Singleton(); } return instance; } }
這種方式具備很好的懶載入(lazy loading),能夠在多執行緒中很好的工作,但是,效率很低,99% 情況下不需要同步。
那麼繼續優化看第三種實現方式 ↓
3、雙檢鎖/雙重校驗鎖
這種方式採用雙鎖機制支援懶載入、執行緒安全且在多執行緒情況下能保持高效能。
/** 單例模式-雙檢鎖/雙重校驗鎖-執行緒安全 */ public class Singleton { // 私有建構函式,防止被例項化 private Singleton(){}; // 單例物件 此處賦值為null,目的是實現延遲載入 private static Singleton instance = null; // 靜態工廠方法,建立唯一可用的物件 public static Singleton getInstance(){ // 雙檢鎖/雙重校驗鎖 if (instance == null) { //同步鎖 synchronized(Singleton.class){ if (instance == null) { instance = new Singleton(); } } } return instance; } }
認真看的同學可能會發現其實這個Singleton 類雖然也加了鎖synchronized 但是並沒有解決多執行緒問題。
試想執行緒A走到第11行程式碼,Singleton 類第一次建立例項,同時執行緒B進來走到第8行程式碼。
這種情況下執行緒B第8行程式碼中 if (instance == null)就很有可能返回false,從而獲取到未初始化完成的 instance。
為什麼 if (instance == null) 會有可能返回false 呢? 這裡就涉及到了JVM編譯器和CPU的指令重排。
指令重排是什麼意思呢?比如java中簡單的一句 instance = new Singleton,會被編譯器編譯成如下JVM指令:
memory =allocate(); //1:分配物件的記憶體空間
ctorInstance(memory); //2:初始化物件
instance =memory; //3:設定instance指向剛分配的記憶體地址
但是這些指令順序並非一成不變,有可能會經過JVM和CPU的優化,指令重排成下面的順序:
memory =allocate(); //1:分配物件的記憶體空間
instance =memory; //3:設定instance指向剛分配的記憶體地址
ctorInstance(memory); //2:初始化物件
當執行緒A執行完1,3時,instance物件還未完成初始化,但已經不再指向null。此時如果執行緒B搶佔到CPU資源,執行 if(instance == null)的結果會是false,從而返回一個沒有初始化完成的instance物件。
如何避免這一情況呢?我們需要在instance物件前面增加一個修飾符volatile。
/** 單例模式-雙檢鎖/雙重校驗鎖-執行緒安全 */ public class Singleton { // 私有建構函式,防止被例項化 private Singleton(){}; // 單例物件 此處賦值為null,目的是實現延遲載入 // 新增 volatile 是為了操作此物件時防止JVM和CPU指令重排 private volatile static Singleton instance = null; // 靜態工廠方法,建立唯一可用的物件 public static Singleton getInstance(){ // 雙檢鎖/雙重校驗鎖 if (instance == null) { //同步鎖 synchronized(Singleton.class){ if (instance == null) { instance = new Singleton(); } } } return instance; } }
volatile修飾符阻止了變數訪問前後順序的指令重排,保證了指令的執行順序。
如此線上程B看來,instance物件的引用要麼指向null,要麼指向一個初始化完畢的Instance,而不會出現某個中間態,保證了安全。
5、登記式/靜態內部類
這種方式通過靜態內部類支援懶載入、執行緒安全且在多執行緒情況下能保持高效能。
/** 單例模式-登記式/靜態內部類-執行緒安全 */ public class Singleton { // 私有建構函式,防止被例項化 private Singleton(){}; // 此處使用一個內部類來維護單例 private static class SingletonFatory{ private static Singleton instance = new Singleton(); } // 靜態工廠方法,獲取唯一例項物件 public static Singleton getInstance(){ return SingletonFatory.instance; } // 如果該物件被用於序列化,可以保證物件在序列化前後保持唯一性 public Object readResolve() { return getInstance(); } }
instance 物件初始化的時機並不是在單例類Singleton被載入的時候,而是在呼叫getInstance方法,使得靜態內部類SingletonFatory 被載入的時候。因此這種實現方式是利用classloader的載入機制來實現懶載入,並保證構建單例的執行緒安全。程式碼中readResolve()方法在下面補充。
6、列舉這種實現方式還沒有被廣泛採用,但這是實現單例模式的最佳方法。它更簡潔,自動支援序列化機制,絕對防止多次例項化(防止反射構造物件,反射構造物件會在下面做具體補充),不過不支援懶載入。
package test; /** 單例模式-列舉-執行緒安全 */ public enum Singleton { INSTANCE; public void whateverMethod(){ System.out.println("列舉型別實現單例模式!"); } public static void main(String[] args) { Singleton.INSTANCE.whateverMethod(); } }
為了好理解點我加了個main方法實現呼叫,這樣一看可能還會有點蒙(大神跳過),下面我寫一種好理解點的。
package test; /** 單例模式-列舉-執行緒安全 */ public class EnumSingleton { // 私有建構函式 private EnumSingleton(){}; public static EnumSingleton getInstance(){ return Singleton.INSTANCE.getInstance(); } //列舉-靜態常量,隱式地用static final修飾過 private enum Singleton{ INSTANCE; private EnumSingleton singleton; //JVM會保證此方法絕對只調用一次 //列舉實際上是類,這裡是構造方法 private Singleton(){ singleton = new EnumSingleton(); } public EnumSingleton getInstance(){ return singleton; } } }
下面做兩點補充:
1、如果該單例類需要序列化則需加 readResolve() 方法,來確保物件在序列化前後保持唯一性;
具體實現在上面第5種實現方式程式碼裡有增加readResolve() 方法。
2、反射構造物件:
以上第1-5種單例實現方式都有一個共同的問題:無法防止利用反射構造物件重複構建物件,下面我們在餓漢式單例模式的基礎上來實現一下反射構造物件。
程式碼可以簡單歸納為三個步驟:
1、獲得單例類的構造器。
2、把構造器設定為可訪問。
3、使用newInstance方法構造物件。
/** 單例模式-餓漢式-執行緒安全 */ public class Singleton3 { // 私有建構函式,防止被例項化 private Singleton3(){} // 單例物件 類載入時建立instance 避免了多執行緒同步問題,但容易產生垃圾物件 private static Singleton3 instance = new Singleton3(); // 靜態工廠方法,獲取唯一可用的物件 public static Singleton3 getInstance() { return instance; } public static void main(String[] args) throws Exception{ //獲得構造器 Constructor con = Singleton.class.getDeclaredConstructor(); //設定為可訪問 con.setAccessible(true); //構造兩個不同的物件 Singleton singleton1 = (Singleton)con.newInstance(); Singleton singleton2 = (Singleton)con.newInstance(); //驗證是否是不同物件 System.out.println(singleton1.equals(singleton2)); } }
執行結果:
false
最後為了確認這兩個物件是否真的是不同的物件,我們使用equals方法進行比較。毫無疑問,比較結果是false。
第6種實現單例模式的方法(列舉)可以有效防止反射構造物件。