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