Java設計模式學習記錄-單例模式
前言
已經介紹和學習了兩個創建型模式了,今天來學習一下另一個非常常見的創建型模式,單例模式。
單例模式也被稱為單件模式(或單體模式),主要作用是控制某個類型的實例數量是一個,而且只有一個。
單例模式
單例模式的實現方式
實現單例模式的方式有很多種,大體上可以劃分為如下兩種。
外部方式
在使用某些全局對象時,做一些“try-Use”的工作。就是如果要使用的這個全局對象不存在,就自己創建一個,把它放到全局的位置上;如果本來就有,則直接拿來使用。
內部實現方式
類型自己控制正常實例的數量,無論客戶程序是否嘗試過了,類型自己自己控制只提供一個實例,客戶程序使用的都是這個現成的唯一實例。
目前隨著集群、多核技術的普遍應用,想通過簡單的類型內部控制失效真正的Singleton越來越難,試圖通過經典單例模式實現分布式環境下的“單例”並不現實。所以目前介紹的這個單例是有語義限制的。
單例模式的特點
雖然單例模式也屬於創建型模式,淡水它是有自己獨特的特點的。
- 單例類只有一個實例。
- 單例類自行創建該實例,在該類內部創建自身的實例對象。
- 向整個系統公開這個實例接口。
還有需要註意的一點,單例模式只關心類實例的創建問題,並不關心具體的業務功能。
單例模式的範圍
目前Java裏面實現的單例是一個ClassLoader及其子ClassLoader的範圍。因為ClassLoader在裝載餓漢式實現的單例類時,會響應地創建一個類的實例。這也說明,如果一個虛擬機裏有多個ClassLoader(雖然說ClassLoader遵循雙親委派模型,但是也會有父加載器處理不了,然後自定義的加載器執行類加載的情況。),而且這些ClassLoader都裝載著某一個類的話,就算這個類是單例,它也會產生很多個實例。如果一個機器上有多個虛擬機,那麽每個虛擬機裏面都應該至少有一個這個類的實例,也就是說整個機器上就有很多個實例,更不會是單例了。
還有一點再次強調,目前討論的單例範圍不適用於集群環境。
單例模式的類型
餓漢式單例
餓漢式單例是指在類被加載的時候,唯一實例已經被創建。
如下代碼的例子:
/** * 餓漢式單例模式 * */ public class HungrySingleton { /** * 定義一個靜態變量用來存儲實例,在類加載的時候創建,只會創建一次。 */ private static HungrySingleton hungrySingleton = new HungrySingleton(); /** * 私有化構造方法,禁止外部創建實例。*/ private HungrySingleton(){ System.out.println("創建實例"); } /** * 外部獲取唯一實例的方法 * @return */ public static HungrySingleton getInstance(){ return hungrySingleton; } }
懶漢式單例
懶漢式單例是指在類加載的時候不創建單例的對象,只有在第一次使用的時候創建,並且在第一次創建後,以後不再創建該類的實例。
如下代碼的例子:
/** * 懶漢式單例 */ public class LazySingleton { /** * 定義一個靜態變量用來存儲實例。 */ private static LazySingleton lazySingleton = null; /** * 私有化構造方法,禁止外部創建實例。 */ private LazySingleton(){} /** * 外部獲取唯一實例的方法
* 當發現沒有初始化的時候,才初始化靜態變量。 * @return */ public static LazySingleton getInstance(){ if(null==lazySingleton){ lazySingleton = new LazySingleton(); } return lazySingleton; } }
登記式單例
登記式單例實際上維護的是一組單例類的實例,將這些實例存在在一個登記薄(例如Map)中,使用已經登記過的實例,直接從登記簿上返回,沒有登記的,則先登記,後返回。
如下代碼例子:
/** * 登記式單例 */ public class RegisterSingleton { /** * 創建一個登記簿,用來存放所有單例對象 */ private static Map<String,RegisterSingleton> registerBook = new HashMap<>(); /** * 私有化構造方法,禁止外部創建實例 */ private RegisterSingleton(){} /** * 註冊實例 * @param name 登記簿上的名字 * @param registerSingleton 登記簿上的實例 */ public static void registerInstance(String name,RegisterSingleton registerSingleton){ if(!registerBook.containsKey(name)){ registerBook.put(name,registerSingleton); } } /** * 獲取實例,如果在未註冊時調用將返回null * @param name 登記簿上的名字 * @return */ public static RegisterSingleton getInstance(String name){ return registerBook.get(name); } }
由於餓漢式的單例在類加載的時候就創建了一個實例,所以這個實例一直都不會變,因此也是線程安全的。但是懶漢式單例就不是線程安全的了,在懶漢式單例中有可能會出現兩個線程創建了兩個不同的實例,因為懶漢式單例中的getInstance()方法不是線程安全的。所以如果想讓懶漢式變成線程安全的,需要在getInstance()方法中加鎖。
如下所示:
/** * 外部獲取唯一實例的方法 * 當發現沒有被初始化的時候,才初始化靜態變量 * @return */ public static synchronized LazySingleton getInstance(){ if(null==lazySingleton){ lazySingleton = new LazySingleton(); } return lazySingleton; }
但是這樣增加的資源消耗,延遲加載的效果雖然達到了,但是在使用的時候資源消耗確更大了,所以不建議這樣用。既要實現線程安全,又要保證延遲加載。基於這樣的問題就出現了另一種方式的單例模式,靜態內部類式單例。
靜態內部類式單例
靜態內部類式單例餓漢式和懶漢式的結合。
如下代碼例子:
/** * 內部靜態類式單例 */ public class StaticClassSingleton { /** * 私有化構造方法,禁止外部創建實例。 */ private StaticClassSingleton(){ System.out.println("創建實例了"); } /** * 私有靜態內部類,只能通過內部調用。 */ private static class SingleClass{ private static StaticClassSingleton singleton = new StaticClassSingleton(); } /** * 外部獲取唯一實例的方法 * @return */ public static StaticClassSingleton getInstance(){ return SingleClass.singleton; } }
雙重檢查加鎖式單例
上面靜態內部類的方式通過結合餓漢式和懶漢式來實現了即延遲加載了又線程安全了。下面也來介紹另一種即實現了延遲加載有保證了線程安全的方式的單例。
如下代碼例子:
/** * 雙重檢查加鎖式單例 */ public class DoubleCheckLockSingleton { /** * 靜態變量,用來存放實例。 */ private volatile static DoubleCheckLockSingleton doubleCheckLockSingleton = null; /** * 私有化構造方法,禁止外部創建實例。 */ private DoubleCheckLockSingleton(){} /** * 雙重檢查加鎖的方式保證線程安全又能獲得到唯一實例 * @return */ public static DoubleCheckLockSingleton getInstance(){ //先檢查實例是否已經存在,不存在則進入代碼塊 if(null == doubleCheckLockSingleton){ synchronized (DoubleCheckLockSingleton.class){ //由於synchronized也是重入鎖,即一個線程有可能多次進入到此同步塊中如果第一次進入時已經創建了實例,那麽第二次進入時就不創建了。 if(null==doubleCheckLockSingleton){ doubleCheckLockSingleton = new DoubleCheckLockSingleton(); } } } return doubleCheckLockSingleton; } }
如上所示,所謂“雙重檢查加鎖”機制,並不是每次進入getInstance()方法都需要加鎖,而是當進入方法後,先檢查實例是否已經存在,如果不存在才進行下面的同步塊,這是第一重檢查,進入同步塊後,再次檢查實例是否已經存在,如果不存在,就在同步塊中創建一個實例,這是第二重檢查。這個過程是只需要同步一次的。
還需要註意的一點是,在使用“雙重檢查加鎖”時,需要在變量上使用關鍵字volatile,這個關鍵字的作用是,被volatile修飾的變量的值不會被本地線程緩存,所有對該變量的讀寫都是直接操作共享內存,從而確保多個線程能正確地處理該變量。可能不了解Java內存模式的朋友不太好理解這句話的意思,可以去看看(JVM學習記錄-Java內存模型(一),JVM學習記錄-Java內存模型(二))了解一下Java內存模型,我簡單說明一下,volatile這個關鍵字可以保證每個線程操作的變量都會被其他線程所看到,就是說如果第一個線程已經創建了實例,但是把創建的這個實例只放在了自己的這個線程中,其他線程是看不到的,這個時候如果其他線程再去判斷實例是否已經存在了實例的時候,發現沒有還是沒有實例就會又創建了一個實例,然後也放在了自己的線程中,如果這樣的話我們寫的單例模式就沒意義了。在JDK1.5以前的版本中對volatile的支持存在問題,可能會導致“雙重檢查加鎖”失敗,所以如果要使用“雙重檢查加鎖”式單例,只能使用JDK1.5以上的版本。
枚舉式單例
在JDK1.5中引入了一個新的特性,枚舉,通過枚舉來實現單例,在目前看來是最佳的方法了。Java的枚舉類型實質上是功能齊全的類,因此可以有自己的屬性和方法。
還是通過代碼示例來解釋吧。
如下代碼例子:
/** * 單元素枚舉實現單例模式 */ public enum EnumSingleton { /** * 必須是單元素,因為一個元素就是一個實例。 */ INSTANCE; /** * 測試方法1 * @return */ public void doSomeThing() { System.out.println("#####測試方法######"); } /** * 測試方法2 * @return */ public String getSomeThing(){ return "獲得到了一些內容"; } }
上面例子中EnumSingleton.INSTANCE就可以獲得到想要的實例了,調用單例的方法可以種EnumSingleotn.INSTANCE.doSomeThing()等方法。
下面來看看枚舉是如何保證單例的:
首先枚舉的構造方法明確是私有的,在使用枚舉實例時會執行構造方法,同時每個枚舉實例都是static final類型的,表明枚舉實例只能被賦值一次,這樣在類初始化的時候就會把實例創建出來,這也說明了枚舉單例,其實是餓漢式單例方式。這樣就用最簡單的代碼既保證了線程安全,又保證了代碼的簡潔。
還有一點很值得註意的是,枚舉實現的單例保證了序列化後的單例安全。除了枚舉式的單例,其他方式的單例,都可能會通過反射或反序列化來創建多個實例。
所以在使用單例的時候最好的辦法就是用枚舉的方式。既簡潔又安全。
Java設計模式學習記錄-單例模式