設計模式—單例模式的六種寫法
確保某個類只有一個例項,而且自行例項化並向整個系統提供這個例項
二、UML結構圖
三、場景
- 需要頻繁的例項化和銷燬的物件;
- 有狀態的工具類物件
- 頻繁訪問資料庫或檔案物件;
- 確保某個類只有一個物件的場景,比如一個物件需要消耗的資源過多,訪問io、資料庫,需要提供全域性配置的場景
四、幾種單例模式
1、餓漢式
宣告靜態時已經初始化,在獲取物件之前就初始化
優點:獲取物件的速度快,執行緒安全(因為虛擬機器保證只會裝載一次,在裝載類的時候是不會發生併發的)
缺點:耗記憶體(若類中有靜態方法,在呼叫靜態方法的時候類就會被載入,類載入的時候就完成了單例的初始化,拖慢速度)
/** * 單例模式:餓漢式 * 在類載入的時候就已經完成了初始化,所以類載入較慢,但獲取物件的速度快 * @author Administrator * */ public class EagerSingleton { //靜態私有成員,已初始化 private static EagerSingleton instance = new EagerSingleton(); //私有建構函式 private EagerSingleton() { } //靜態,不用同步(類載入時已初始化,不會有多執行緒的問題)public static EagerSingleton getInstance() { return instance; } }
2、懶漢式
synchronized同步鎖: 多執行緒下保證單例物件唯一性
優點:單例只有在使用時才被例項化,一定程度上節約了資源
缺點:加入synchronized關鍵字,造成不必要的同步開銷。不建議使用。
/** * 單例模式:懶漢式(執行緒安全的懶漢式) * 比較懶,在類載入時,不建立例項,因此類載入速度快,但執行時獲取物件的速度慢 * @author Administrator * */ publicclass LazySingleton { //靜態私有成員,沒有初始化 private static LazySingleton instance = null; //私有建構函式 private LazySingleton() { } //靜態,同步,公開訪問點 public static synchronized LazySingleton getInstace() { if(instance == null) { instance = new LazySingleton(); } return instance; } }
3、Double Check Lock(DCL)實現單例(使用最多的單例實現之一)
雙重鎖定體現在兩次判空
優點:既能保證執行緒安全,且單例物件初始化後呼叫getInstance不進行同步鎖,資源利用率高
缺點:第一次載入稍慢,由於Java記憶體模型一些原因偶爾會失敗,在高併發環境下也有一定的缺陷,但概率很小。
/** * 單例模式:雙重鎖定式 * @author Administrator * */ public class SingletonKerriganD { //這裡加volatitle是為了避免DCL失效 private volatile static SingletonKerriganD instance = null; //私有建構函式 private SingletonKerriganD() { } /** * DCL對instance進行了兩次null判斷 * 第一層判斷主要是為了避免不必要的同步 * 第二層判斷則是為了在null的情況下建立例項 * @return */ public static SingletonKerriganD getInstance() { if(instance == null) { synchronized (SingletonKerriganD.class) { if(instance == null) { instance = new SingletonKerriganD(); } } } return instance; } }
什麼是DCL失效問題?
假如執行緒A執行到instance = new SingletonKerriganD(),大致做了如下三件事:
- 給例項分配記憶體
- 呼叫建構函式,初始化成員欄位
- 將instance 物件指向分配的記憶體空間(此時sInstance不是null)
如果執行順序是1-3-2,那多執行緒下,A執行緒先執行3,2還沒執行的時候,此時instance!=null,這時候,B執行緒直接取走instance ,使用會出錯,難以追蹤。JDK1.5及之後的volatile 解決了DCL失效問題(雙重鎖定失效)
4、靜態內部類單例模式 在呼叫 SingletonHolder.instance 的時候,才會對單例進行初始化優點:執行緒安全、保證單例物件唯一性,同時也延遲了單例的例項化
缺點:需要兩個類去做到這一點,雖然不會建立靜態內部類的物件,但是其 Class 物件還是會被建立,而且是屬於永久代的物件。
/** * 單例模式:靜態內部類式 * @author Administrator * */ public class SingletonInner { //私有建構函式 private SingletonInner() { } //在呼叫SingletonHolder.instance的時候,才會對單例進行初始化 public static class SingletonHolder{ private final static SingletonInner instance = new SingletonInner(); } public static SingletonInner getInstance() { return SingletonHolder.instance; } }
這種方式如何保證單例且執行緒安全?
當getInstance方法第一次被呼叫的時候,它第一次讀取SingletonHolder.instance,內部類SingletonHolder類得到初始化;而這個類在裝載並被初始化的時候,會初始化它的靜態域,從而建立Singleton的例項,由於是靜態的域,因此只會在虛擬機器裝載類的時候初始化一次,並由虛擬機器來保證它的執行緒安全性。 這個模式的優勢在於,getInstance方法並沒有被同步,並且只是執行一個域的訪問,因此延遲初始化並沒有增加任何訪問成本。
這種方式能否避免反射入侵?
答案是:不能。網上很多介紹到靜態內部類的單例模式的優點會提到“通過反射,是不能從外部類獲取內部類的屬性的。 所以這種形式,很好的避免了反射入侵”,這是錯誤的,反射是可以獲取內部類的屬性(想了解更多反射的知識請看 java反射全解),入侵單例模式根本不在話下
【注意】:上述四種方法要杜絕在被反序列化時重新宣告物件,需要加入如下方法:private Object readResolve() throws ObjectStreamException{ return sInstance; }
為什麼呢?因為當JVM從記憶體中反序列化地"組裝"一個新物件時,自動呼叫 readResolve方法來返回我們指定好的物件
5、列舉單例
優點:執行緒安全,防止被反序列化
缺點:列舉相對耗記憶體
public enum SingletonEnum { instance; public void doThing(){ } }
只要SingletonEnum.INSTANCE即可獲得所要例項。
這種方式如何保證單例?
首先,在列舉中我們明確了構造方法限制為私有,在我們訪問列舉例項時會執行構造方法,同時每個列舉例項都是static final型別的,也就表明只能被例項化一次。在呼叫構造方法時,我們的單例被例項化。 也就是說,因為enum中的例項被保證只會被例項化一次,所以我們的INSTANCE也被保證例項化一次。
上面示例中生成的位元組碼檔案對instance的描述如下:... public static final eft.reflex.SingletonEnum instance; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM ...
可以看出,會自動生成 ACC_STATIC, ACC_FINAL這兩個修飾符
列舉型別為什麼是執行緒安全的?
我們定義的一個列舉,在第一次被真正用到的時候,會被虛擬機器載入並初始化,而這個初始化過程是執行緒安全的。而我們知道,解決單例的併發問題,主要解決的就是初始化過程中的執行緒安全問題。所以,由於列舉的以上特性,列舉實現的單例是天生執行緒安全的。
6、使用容器實現單例模式 在程式的初始化,將多個單例型別注入到一個統一管理的類中,使用時通過key來獲取對應型別的物件,這種方式使得我們可以管理多種型別的單例,並且在使用時可以通過統一的介面進行操作。這種方式是利用了Map的key唯一性來保證單例。import java.util.HashMap; import java.util.Map; /** * 單例模式:容器模式 * @author Administrator * */ public class SingletonManager { private static Map<String, Object> map = new HashMap<String, Object>(); private SingletonManager() { } public static void registerService(String key, Object instance) { if(!map.containsKey(key)) { map.put(key, instance); } } public static Object getService(String key) { return map.get(key); } }
五、總結
所有單例模式需要處理得問題都是:
- 將建構函式私有化
- 通過靜態方法獲取一個唯一例項
- 保證執行緒安全
- 防止反序列化造成的新例項等。
推薦使用:DCL、靜態內部類、列舉
單例模式優點
- 只有一個物件,記憶體開支少、效能好(當一個物件的產生需要比較多的資源,如讀取配置、產生其他依賴物件時,可以通過應用啟動時直接產生一個單例物件,讓其永駐記憶體的方式解決)
- 避免對資源的多重佔用(一個寫檔案操作,只有一個例項存在記憶體中,避免對同一個資原始檔同時寫操作)
- 在系統設定全域性訪問點,優化和共享資源訪問(如:設計一個單例類,負責所有資料表的對映處理)
單例模式缺點
- 一般沒有介面,擴充套件難
- android中,單例物件持有Context容易記憶體洩露,此時需要注意傳給單例物件的Context最好是Application Context