標籤:reg 安全 共用記憶體 前言 let none 簡單 意義 有一個
前言
已經介紹和學習了兩個建立型模式了,今天來學習一下另一個非常常見的建立型模式,單例模式。
單例模式也被稱為單件模式(或單體模式),主要作用是控制某個類型的執行個體數量是一個,而且只有一個。
單例模式單例模式的實現方式
實現單例模式的方式有很多種,大體上可以劃分為如下兩種。
外部方式
在使用某些全域對象時,做一些“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設計模式學習記錄-單例模式