設計模式系列之單例模式(Singleton Pattern)——確保物件的唯一性

行無際發表於2020-05-23

說明:設計模式系列文章是讀劉偉所著《設計模式的藝術之道(軟體開發人員內功修煉之道)》一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的部落格https://blog.csdn.net/LoveLion/article/details/17517213

模式概述

模式定義

實際開發中,我們會遇到這樣的情況,為了節約系統資源或者資料的一致性(比如說全域性的Config、攜帶上下文資訊的Context等等),有時需要確保系統中某個類只有唯一一個例項,當這個唯一例項建立成功之後,我們無法再建立一個同型別的其他物件,所有的操作都只能基於這個唯一例項。為了確保物件的唯一性,我們可以通過單例模式來實現,這就是單例模式的動機所在。

單例模式(Singleton Pattern): 確保某一個類只有一個例項,而且自己例項化並向整個系統提供這個例項,這個類稱為單例類,它提供全域性訪問的方法。單例模式是一種物件建立型模式。

單例模式有三個要點:

  1. 某個類只能有一個例項
  2. 它必須自行建立這個例項
  3. 它必須自行向整個系統提供這個例項

模式結構圖

單例模式是結構最簡單的設計模式一,在它的核心結構中只包含一個被稱為單例類的特殊類。單例模式結構圖如下所示:

單例模式結構圖

單例模式結構圖中只包含一個單例角色:

  • Singleton(單例):在單例類的內部實現只生成一個例項,同時它提供一個靜態的getInstance()工廠方法,讓客戶可以訪問它的唯一例項;為了防止在外部對其例項化,將其建構函式設計為私有;在單例類內部定義了一個Singleton型別的靜態物件,作為外部共享的唯一例項。

餓漢式單例與懶漢式單例

餓漢式單例

餓漢式單例類是實現起來最簡單的單例類。由於在定義靜態變數的時候例項化單例類,因此在類載入的時候就已經建立了單例物件,典型程式碼如下:

public class EagerSingleton { 
    private static final EagerSingleton instance = new EagerSingleton(); 
    private EagerSingleton() { } 
 
    public static EagerSingleton getInstance() {
        return instance; 
    }   
}

懶漢式單例

懶漢式單例在第一次呼叫getInstance()方法時例項化,在類載入時並不自行例項化,這種技術又稱為延遲載入(Lazy Load)或者懶載入技術,即需要的時候再載入例項,為避免多執行緒環境下同時呼叫getInstance()方法從而生成多個例項,需要確保執行緒安全,相應實現也就有多種方式。

第一種方法可以使用關鍵字synchronized,程式碼實現如下:

public class LazySingleton { 
    private static LazySingleton instance = null; 
 
    private LazySingleton() { } 
 
    public synchronized static LazySingleton getInstance() { 
        if (instance == null) {
            instance = new LazySingleton(); 
        }
        return instance; 
    }
}

getInstance()方法前面增加了關鍵字synchronized進行同步,以處理多執行緒同時訪問的安全問題。我們知道使用synchronized關鍵字最好是在離共享資源最近的位置加鎖,這樣同步帶來的效能影響會減小。所以讓人感覺上面的實現可以優化為如下程式碼:

public static LazySingleton getInstance() { 
    if (instance == null) {
        synchronized (LazySingleton.class) {
            instance = new LazySingleton(); 
        }
    }
    return instance; 
}

問題貌似得以解決,事實並非如此。如果使用以上程式碼來實現單例,還是會存在單例物件不唯一。原因如下:
假如在某一瞬間執行緒A執行緒B都在呼叫getInstance()方法,此時instance物件為null值,均能通過instance == null的判斷。由於實現了synchronized加鎖機制,執行緒A進入synchronized修飾的程式碼塊中執行例項建立程式碼,執行緒B處於排隊等待狀態,必須等待執行緒A執行完畢後才可以進入synchronized修飾的程式碼塊。但當A執行完畢時,執行緒B並不知道例項已經建立,將繼續建立新的例項,導致產生多個單例物件,違背單例模式的設計思想,因此需要進行進一步改進,在synchronized中再進行一次(instance == null)判斷,這種方式稱為雙重檢查鎖定(Double-Check Locking)。使用雙重檢查鎖定實現的懶漢式單例類典型程式碼如下所示:

public class LazySingleton { 
    private volatile static LazySingleton instance = null; 
 
    private LazySingleton() { } 
 
    public static LazySingleton getInstance() { 
        // 第一重判斷
        if (instance == null) {
            // 使用synchronized關鍵字加鎖
            synchronized (LazySingleton.class) {
                //第二重判斷
                if (instance == null) {
                    instance = new LazySingleton(); //建立單例例項
                }
            }
        }
        return instance; 
    }
}

需要注意的是,如果使用雙重檢查鎖定來實現懶漢式單例類,最好在靜態成員變數instance之前增加修飾符volatile,被volatile修飾的變數可以保證多執行緒環境下的可見性以及禁止指令重排序。由於volatile關鍵字會遮蔽Java虛擬機器所做的一些優化,可能對執行效率稍微有些影響,因此使用雙重檢查鎖定來實現單例模式也不一定是最完美的實現方式。

如果是java語言的程式,還可以使用靜態內部類的方式實現。程式碼如下:

public class Singleton {
	private Singleton() {
	}
	
	private static class HolderClass {
    final static Singleton instance = new Singleton();
	}
	
	public static Singleton getInstance() {
	  return HolderClass.instance;
	}
}

由於靜態單例物件沒有作為Singleton的成員變數直接例項化,因此類載入時不會例項化Singleton,第一次呼叫getInstance()時將載入內部類HolderClass,在該內部類中定義了一個static型別的變數instance,此時會首先初始化這個變數,由Java虛擬機器來保證其執行緒安全性,確保該成員變數只初始化一次。

模式應用

模式在JDK中的應用

在JDK中,java.lang.Runtime使用了餓漢式單例,如下:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
}

模式在開源專案中的應用

Spring框架中許多地方使用了單例模式,這裡隨便舉個例子,如org.springframework.aop.framework.ProxyFactoryBean中的部分程式碼如下:

/**
 * Return the singleton instance of this class's proxy object,
 * lazily creating it if it hasn't been created already.
 * @return the shared singleton proxy
 */
private synchronized Object getSingletonInstance() {
  if (this.singletonInstance == null) {
    this.targetSource = freshTargetSource();
    if (this.autodetectInterfaces && getProxiedInterfaces().length == 0 && !isProxyTargetClass()) {
      // Rely on AOP infrastructure to tell us what interfaces to proxy.
      Class<?> targetClass = getTargetClass();
      if (targetClass == null) {
        throw new FactoryBeanNotInitializedException("Cannot determine target class for proxy");
      }
      setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader));
    }
    // Initialize the shared singleton instance.
    super.setFrozen(this.freezeProxy);
    this.singletonInstance = getProxy(createAopProxy());
  }
  return this.singletonInstance;
}

模式總結

單例模式作為一種目標明確、結構簡單、理解容易的設計模式,在軟體開發中使用頻率相當高,在很多應用軟體和框架中都得以廣泛應用。

主要優點

(1) 單例模式提供了對唯一例項的受控訪問。因為單例類封裝了它的唯一例項,所以它可以嚴格控制客戶怎樣以及何時訪問它。

(2) 由於在系統記憶體中只存在一個物件,因此可以節約系統資源,對於一些需要頻繁建立和銷燬的物件單例模式無疑可以提高系統的效能。

(3) 允許可變數目的例項。基於單例模式我們可以進行擴充套件,使用與單例控制相似的方法來獲得指定個數的物件例項,既節省系統資源,又解決了單例單例物件共享過多有損效能的問題。

適用場景

在以下情況下可以考慮使用單例模式:

(1) 系統只需要一個例項物件,如系統要求提供一個唯一的序列號生成器或資源管理器,或者需要考慮資源消耗太大而只允許建立一個物件。

(2) 客戶呼叫類的單個例項只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該例項。

相關文章