從零開始學設計模式(三)——單例模式(Singleton Pattern)
單例模式(Singleton Pattern)
單例模式也屬於建立型模式,難度等級為初級,是Java中最簡單和最常見的設計模式之一。由於其常見性,單例模式的實現方法衍生出很多種,不同的實現方式在延遲載入、執行緒安全、效能上各有千秋,後面我們會在程式程式碼說明章節中來具體分析。
單例模式涉及到一個單一的類,該類負責建立自己的物件,同時確保只有單個物件被建立。這個類提供了一種訪問其唯一物件的方式,可以直接訪問,不需要例項化該類的物件。
意圖
確保一個類只有一個例項,並提供對它的全域性訪問點
主要解決:一個全域性使用的類頻繁地建立與銷燬。
何時使用:當你想控制例項數目,節省系統資源的時候。
如何解決:判斷系統是否已經有這個單例,如果有則返回,如果沒有則建立。
關鍵程式碼:建構函式是私有的。
解釋
現實世界的例子
只有一座象牙塔可以讓巫師們研究他們的魔法。巫師們總是使用同樣的魔法象牙塔。這裡的象牙塔是單例
簡而言之
確保一個特定類永遠只建立一個物件例項
維基百科
In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system.(在軟體工程中,singleton模式是一種軟體設計模式,它將類的例項化限制為一個物件。當只需要一個物件來協調整個系統的動作時,這很有用)
程式程式碼說明
1、懶漢式,執行緒不安全的
public class Singleton{ private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
這種方式是最基本的實現方式,但由於其不支援多執行緒,getInstance方法沒有加鎖,併發的情況下可能產生多個instance例項,所以嚴格意義上來說它並不算單例模式,但它做到了Lazy初始化。
這種方式目前已不建議使用
2、懶漢式,執行緒安全的
以上面巫師們的象牙塔為例子
/** * Thread-safe Singleton class. The instance is lazily initialized and thus needs synchronization * mechanism. * * Note: if created by reflection then a singleton will not be created but multiple options in the * same classloader */ public final class ThreadSafeLazyLoadedIvoryTower { private static ThreadSafeLazyLoadedIvoryTower instance; private ThreadSafeLazyLoadedIvoryTower() { // protect against instantiation via reflection if (instance == null) { instance = this; } else { throw new IllegalStateException("Already initialized."); } } /** * The instance gets created only when it is called for first time. Lazy-loading */ public static synchronized ThreadSafeLazyLoadedIvoryTower getInstance() { if (instance == null) { instance = new ThreadSafeLazyLoadedIvoryTower(); } return instance; } }
這種方式具備很好的 lazy loading,能夠在多執行緒中很好的工作,但是由於加了同步鎖,效率很低,99%情況下不需要同步。對於getInstance()的效能要求不是很高的應用程式可以考慮使用(該方法使用不頻繁)
3、雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking)
此方式需在JDK1.4之後,採用雙鎖機制,安全且在多執行緒情況下保持了較高的效能。
public final class ThreadSafeDoubleCheckLocking { private static volatile ThreadSafeDoubleCheckLocking instance; /** * private constructor to prevent client from instantiating. */ private ThreadSafeDoubleCheckLocking() { // to prevent instantiating by Reflection call if (instance != null) { throw new IllegalStateException("Already initialized."); } } /** * Public accessor. * * @return an instance of the class. */ public static ThreadSafeDoubleCheckLocking getInstance() { // local variable increases performance by 25 percent // Joshua Bloch "Effective Java, Second Edition", p. 283-284 ThreadSafeDoubleCheckLocking result = instance; // Check if singleton instance is initialized. If it is initialized then we can return the instance. if (result == null) { // It is not initialized but we cannot be sure because some other thread might have initialized it // in the meanwhile. So to make sure we need to lock on an object to get mutual exclusion. synchronized (ThreadSafeDoubleCheckLocking.class) { // Again assign the instance to local variable to check if it was initialized by some other thread // while current thread was blocked to enter the locked zone. If it was initialized then we can // return the previously created instance just like the previous null check. result = instance; if (result == null) { // The instance is still not initialized so we can safely (no other thread can enter this zone) // create an instance and make it our singleton instance. instance = result = new ThreadSafeDoubleCheckLocking(); } } } return result; } }
4、餓漢式
這種方式比較常用,但容易產生垃圾物件,沒有鎖機制,執行效率很高,但未實現Lazy初始化。
** * Singleton class. Eagerly initialized static instance guarantees thread safety. */ public final class IvoryTower { /** * Private constructor so nobody can instantiate the class. */ private IvoryTower() {} /** * Static to class instance of the class. */ private static final IvoryTower INSTANCE = new IvoryTower(); /** * To be called by user to obtain instance of the class. * * @return instance of the singleton. */ public static IvoryTower getInstance() { return INSTANCE; } }
基於classloader機制避免了多執行緒的同步問題,不過由於IvoryTower類在裝載時就已經例項化了,顯然沒有達到lazy loading的效果。
5、登記式/靜態內部類
這種方式可以達到上面第三種雙檢鎖一樣的功效,具備了延遲載入和執行緒安全,且實現簡單。
對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式。這種方式適用於靜態域的情況,雙檢鎖方式可在例項域需要延遲初始化時使用。
public final class InitializingOnDemandHolderIdiom { /** * Private constructor. */ private InitializingOnDemandHolderIdiom() {} /** * @return Singleton instance */ public static InitializingOnDemandHolderIdiom getInstance() { return HelperHolder.INSTANCE; } /** * Provides the lazy-loaded Singleton instance. */ private static class HelperHolder { private static final InitializingOnDemandHolderIdiom INSTANCE = new InitializingOnDemandHolderIdiom(); } }
這種方式同樣利用了 classloader 機制來保證初始化 instance 時只有一個執行緒,它跟第 4 種方式不同的是:第 4種方式只要 IvoryTower 類被裝載了,那麼 instance 就會被例項化(沒有達到 lazy loading 效果),而這種方式是 InitializingOnDemandHolderIdiom 類被裝載了,instance 不一定被初始化。因為 HelperHolder 類沒有被主動使用,只有通過顯式呼叫 getInstance 方法時,才會顯式裝載 HelperHolder 類,從而例項化 instance。想象一下,如果例項化 instance 很消耗資源,所以我們想讓它延遲載入,這種方式相比第 4 種方式就顯得更合理。
此方式對JDK版本也無任何要求,在現在能看到的JDK版本下都支援。
6、列舉
Joshua Bloch, Effective Java 2nd Edition p.18 中提出了使用列舉類來實現單例模式,書中指出這是最好的一種方式來實現單例。
A single-element enum type is the best way to implement a singleton
/** * Enum based singleton implementation. Effective Java 2nd Edition (Joshua Bloch) p. 18 * * This implementation is thread safe, however adding any other method and its thread safety * is developers responsibility. */ public enum EnumIvoryTower { INSTANCE; public void whateverMethod() { // .... do something } @Override public String toString() { return getDeclaringClass().getCanonicalName() + "@" + hashCode(); } }
然後我們使用它
EnumIvoryTower enumIvoryTower1 = EnumIvoryTower.INSTANCE; EnumIvoryTower enumIvoryTower2 = EnumIvoryTower.INSTANCE; assertEquals(enumIvoryTower1, enumIvoryTower2); // true
總結一下:一般情況下,方式1和2不建議使用,在明確不需要實現lazy loading的時候可以優先選擇方式4.在需要lazy loading的時候,可以選擇方式3和5。如果還涉及到反序列化建立物件時可以使用最佳方式6
應用場景
當遇到如下情況時可考慮使用單例模式:
- 一個類必須只有一個例項,客戶端必須可以從眾所周知的通道訪問它
- 一個單例項應該可以被子類可擴充套件,並且客戶端能夠使用擴充套件例項而無需修改其程式碼
使用場景
- 日誌記錄類
- 管理一個資料庫連線
- 檔案管理器
Java中的現例項子
- ofollow,noindex">java.lang.Runtime#getRuntime()
- java.awt.Desktop#getDesktop()
- java.lang.System#getSecurityManager()
優缺點
優點:
1、在記憶體裡只有一個例項,減少了記憶體的開銷,尤其是頻繁的建立和銷燬例項(比如管理學院首頁頁面快取)。
2、避免對資源的多重佔用(比如寫檔案操作)。
缺點:
1、沒有介面,不能繼承,一個類應該只關心內部邏輯,而不關心外面怎麼樣來例項化。
2、通過控制自己的建立和生命週期,違反了單一責任原則SRP(Single Responsibility Principle)
3、建立緊密耦合的程式碼,單例模式的客戶端變得難以測試
寫在最後
單例模式的使用在Java應用中是非常普遍的,Spring 管理bean 例項預設配置就是單例模式。
下一章節我將介紹建造者模式(Builder Pattern)
碼字不易,各位看官如果喜歡的話,請給點個贊 :heart:,關注下我,我將努力持續不斷的更新