一、定義
一個類只有一個實例,且該類能自行創建這個實例的一種模式。
二、單例模式舉例
例如,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詳解 |