一、定義
一個類只有一個實體,且該類能自行創建這個實體的一種模式,
二、單例模式舉例
例如,Windows 中只能打開一個任務管理器,這樣可以避免因打開多個任務管理器視窗而造成記憶體資源的浪費,或出現各個視窗顯示內容的不一致等錯誤,
在計算機系統中,還有 Windows 的回收站、作業系統中的檔案系統、多執行緒中的執行緒池、顯卡的驅動程式物件、列印機的后臺處理服務、應用程式的日志物件、資料庫的連接池、網站的計數器、Web 應用的配置物件、應用程式中的對話框、系統中的快取等常常被設計成單例,
J2EE 標準中的ServletContext 和 ServletContextConfig、Spring框架應用中的 ApplicationContext、資料庫中的連接池等也都是單例模式,
三、特點及優缺點
特點:
-
單例類只有一個實體物件;
-
該單例物件必須由單例類自行創建;
-
單例類對外提供一個訪問該單例的全域訪問點,
優點:
-
單例模式可以保證記憶體里只有一個實體,減少了記憶體的開銷,
-
可以避免對資源的多重占用,
-
單例模式設定全域訪問點,可以優化和共享資源的訪問,
缺點:
-
單例模式一般沒有介面,擴展困難,如果要擴展,則除了修改原來的代碼,沒有第二種途徑,違背開閉原則,
-
在并發測驗中,單例模式不利于代碼除錯,在除錯程序中,如果單例中的代碼沒有執行完,也不能模擬生成一個新的物件,
-
單例模式的功能代碼通常寫在一個類中,如果功能設計不合理,則很容易違背單一職責原則,
四、單例模式的幾種實作方式
單例實作把握住一個原則即可:類的建構式設為私有的,外部類就無法呼叫該建構式,也就無法生成多個實體,這時該類自身必須定義一個靜態私有實體,并向外提供一個靜態的公有函式用于創建或獲取該靜態私有實體,
要點:
-
構造方法私有化;
-
實體化的變數參考私有化;
-
獲取實體的方法共有
第1種:餓漢模式
餓漢模式就是在類加載時,就把單例物件加載出來,實作如下:
/** * 要點:1.類加載時就創建物件 * 2.構造方法私有化 * 3.提供私有成員變數 * 4.提供對外獲取方法 */ public class HungrySingleton { //類加載時就創建物件 private static HungrySingleton singleton=new HungrySingleton(); //提供私有構造器 private HungrySingleton(){ } //提供對外獲取方法,一般為靜態 public static HungrySingleton getInstance(){ return singleton; } }
第2種:懶漢模式
懶漢模式就是懶加載機制,當有地方用單例物件時,再創建物件,如果一直沒有用,則不創建單例物件,代碼如下:
/** * 要點:1.使用時創建物件 * 2.構造方法私有化 * 3.提供私有成員變數 * 4.提供對外獲取方法,注意執行緒安全問題 */ public class LazySingletom { //創建私有變數,但是不new物件 private static LazySingletom singletom=null; //私有構造器 private LazySingletom(){ } //提供對外獲取方法,考慮到執行緒安全,用鎖 public static synchronized LazySingletom getInstance(){ if(singletom==null){ singletom=new LazySingletom(); } return singletom; } }
第3種:雙重檢查鎖模式
在懶漢式方式中,synchronized鎖住了整個方法,這影響了效率,針對此問題,設計出了雙重檢查鎖機制
/** * 雙重檢查鎖機制:1.使用時創建物件 * * 2.構造方法私有化 * * 3.提供私有成員變數 * * 4.提供對外獲取方法,執行緒安全放在方法內判斷 */ public class Singleton { private static Singleton singleton; private Singleton(){ } public static Singleton getInstance(){ if(singleton==null){ synchronized (Singleton.class){ if(singleton==null){ singleton=new Singleton(); } } } return singleton; } }
第4種:列舉實作
利用列舉實作單例,簡單又簡便,代碼如下:
/** * 列舉實作單例模式 */ public enum EnumSingleton { //定義列舉實體,這就是一個單例物件 INSTANCE; /** * 列舉是一種特殊的類,可以定義類里的成員方法,屬性等特征,可以任意定義東西 */ public void getDes(){ System.out.println("列舉單例模式"); } }
對于列舉不了解的同學,可以閱讀這篇文章熟悉列舉:《JAVA中列舉Enum詳解 》
五、序列化和反射,對單例造成的影響
上述講解了單例模式的幾種實作方式,但是有些實作方式存在著漏洞,反射和序列化操作,會破壞單例,生成多個物件,下面我們來進行說明和講解,
首先,我們看反射,對上面幾種方式造成的影響,
我們知道,通過反射,可以獲得類里的私有屬性,包括私有構造器,所以,無論是惡漢式也好,懶漢式也好,還是雙重檢查鎖模式也好,我們都可以用反射,來獲得其私有構造器,然后進行物件的創建,這樣,我們就可以創建出多個物件了,所以,反射,對這三種模式會造成危害,代碼如下:
import java.lang.reflect.Constructor; /** * 我們拿餓漢模式來演示反射對單例的破壞 */ public class ReflectSingleton { public static void main(String[] args) throws Exception{ //通過單例本身拿到單例物件 HungrySingleton singleton=HungrySingleton.getInstance(); System.out.println(singleton); //通過反射拿到單例物件 Class clzz= HungrySingleton.class; Constructor<HungrySingleton> declaredConstructor = clzz.getDeclaredConstructor(); declaredConstructor.setAccessible(true); HungrySingleton singletonReflect = declaredConstructor.newInstance(); System.out.println(singletonReflect); } }
運行main方法,查看運行結果:

可以看到兩個物件的地址值不一致,說明是兩個物件,破壞了單例模式,
那么我們如何改造呢?就餓漢模式而言,我們在私有構造器里做判斷,如果私有成員變數不是null,則拋出例外,阻止通過反射創建新物件,改造后的代碼如下:
/** * 要點:1.類加載時就創建物件 * 2.構造方法私有化 * 3.提供私有成員變數 * 4.提供對外獲取方法 */ public class HungrySingleton { //類加載時就創建物件 private static HungrySingleton singleton=new HungrySingleton(); //提供私有構造器 private HungrySingleton(){ if(singleton!=null){ throw new RuntimeException("禁止通過反射創建單例物件"); } } //提供對外獲取方法,一般為靜態 public static HungrySingleton getInstance(){ return singleton; } }
這樣,我們就可以防止反射破壞餓漢式單例了,但是對于懶漢式和雙重檢查鎖模式,不能這么改造,來阻止反射破壞單例,因為單例物件不是第一時間創建的,如果第一時間通過反射獲取私有構造,這時私有成員變數是null,那么,就能通過反射,創建出來物件了,當有程式呼叫單例的getInstance()方法時,又會創建出一個物件,就破壞了單例,所以,對于懶漢式和雙重檢查鎖模式,無法避免反射的危害,
對于列舉模式而言,我們無法通過反射獲取列舉的構造器,因為列舉的構造器,只能通過jvm呼叫,所以,列舉模式無需改造,可以防止單例的破壞,
下面,我們講序列化,對單例造成的影響,如果我們的單例,不需要實體化,則不用考慮該問題,但是如果單例類實作了Serializable介面,則單例模式會有問題,我們來補充一下序列化的知識:
1.每個類可以實作readObject、writeObject方法實作自己的序列化策略,
2.任何一個readObject方法,不管是顯式的還是默認的,它都會回傳一個新建的實體,這個新建的實體不同于該類初始化時創建的實體
3.每個類可以實作private Object readResolve()方法,在呼叫readObject方法之后,如果存在readResolve方法則自動呼叫該方法,readResolve將對readObject的結果進行處理,而最終readResolve的處理結果將作為readObject的結果回傳,readResolve的目的是保護性恢復物件,其最重要的應用就是保護性恢復單例、列舉型別的物件,
由上面的,我們可以在單例類里自定義readResolve方法,回傳我們自己定義的單例,來保證序列化對單例沒有影響,
需要注意的是,jdk對列舉型別的序列化,已經做了單例的機制,所以,在列舉模式中,自動規避了序列化造成的問題,
經驗之談:幾種模式中,雖然列舉模式是效果最好,沒有缺陷的一種方式,但是我們沒有必要所有的單例模式都用列舉,如果對性能沒有很高要求,餓漢式是一個不錯的選擇,如果對性能有要求,雙重檢查鎖機制是個不錯的選擇,
| 標題 | 發布狀態 | 評論數 | 閱讀數 | 操作 | 操作 |
|---|---|---|---|---|---|
| JAVA中列舉Enum詳解 |
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/267280.html
標籤:Java
上一篇:Java 查找演算法
