已經介紹和學習了兩個建立型模式了,今天來學習一下另外一個很是常見的建立型模式,單例模式。html
單例模式也被稱爲單件模式(或單體模式),主要做用是控制某個類型的實例數量是一個,並且只有一個。緩存
實現單例模式的方式有不少種,大致上能夠劃分爲以下兩種。安全
在使用某些全局對象時,作一些「try-Use」的工做。就是若是要使用的這個全局對象不存在,就本身建立一個,把它放到全局的位置上;若是原本就有,則直接拿來使用。分佈式
類型本身控制正常實例的數量,不管客戶程序是否嘗試過了,類型本身本身控制只提供一個實例,客戶程序使用的都是這個現成的惟一實例。學習
目前隨着集羣、多核技術的廣泛應用,想經過簡單的類型內部控制失效真正的Singleton愈來愈難,試圖經過經典單例模式實現分佈式環境下的「單例」並不現實。因此目前介紹的這個單例是有語義限制的。測試
雖然單例模式也屬於建立型模式,淡水它是有本身獨特的特色的。spa
還有須要注意的一點,單例模式只關心類實例的建立問題,並不關心具體的業務功能。線程
目前Java裏面實現的單例是一個ClassLoader及其子ClassLoader的範圍。由於ClassLoader在裝載餓漢式實現的單例類時,會響應地建立一個類的實例。這也說明,若是一個虛擬機裏有多個ClassLoader(雖說ClassLoader遵循雙親委派模型,可是也會有父加載器處理不了,而後自定義的加載器執行類加載的狀況。),並且這些ClassLoader都裝載着某一個類的話,就算這個類是單例,它也會產生不少個實例。若是一個機器上有多個虛擬機,那麼每一個虛擬機裏面都應該至少有一個這個類的實例,也就是說整個機器上就有不少個實例,更不會是單例了。code
還有一點再次強調,目前討論的單例範圍不適用於集羣環境。htm
餓漢式單例是指在類被加載的時候,惟一實例已經被建立。
以下代碼的例子:
/** * 餓漢式單例模式 * */ 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類型的,代表枚舉實例只能被賦值一次,這樣在類初始化的時候就會把實例建立出來,這也說明了枚舉單例,實際上是餓漢式單例方式。這樣就用最簡單的代碼既保證了線程安全,又保證了代碼的簡潔。
還有一點很值得注意的是,枚舉實現的單例保證了序列化後的單例安全。除了枚舉式的單例,其餘方式的單例,均可能會經過反射或反序列化來建立多個實例。
因此在使用單例的時候最好的辦法就是用枚舉的方式。既簡潔又安全。