簡介
一個類只允許創建一個物件(或實體),那么這個類就是一個單例類,這種設計模式稱作單例設計模式(Singleton Design Pattern),簡稱單例模式,
單例模式保證系統記憶體中只存在一個物件,非常節省系統資源,對于一些需要頻繁銷毀的物件,使用單例模式可以提高系統性能,
一個普通單例模式的實作方式主要是以下三個步驟:
- 將單例類的構造方法定義為私有方法,禁止外部直接呼叫構造方法來實體化單例類的物件;
- 在類的內部創建并保存類的唯一實體,并設定成私有變數,禁止外部直接呼叫這個實體變數;
- 創建一個公開的靜態方法,對外暴露類的唯一實體,
具體實作
餓漢式
餓漢式的實作方式就是,在類裝載的期間,將類的實體初始化好,然后通過靜態方法拿到實體化的物件,
對應的 Java 代碼片段如下:
public class Singleton {
// 靜態實體化
private static final Singleton instance = new Singleton();
// 構造器私有化
private Singleton() {}
// 公有靜態方法,回傳實體物件
public static Singleton getInstance() {
return instance;
}
}
除了通過使用靜態常量初始化實體的方式以外,還可以通過靜態代碼塊的方式實作餓漢式單例模式,
對應的 Java 代碼片段如下:
public class Singleton {
// 靜態變數
private static final Singleton instance;
// 構造器私有化
private Singleton() {}
// 靜態代碼塊
static {
instance = new Singleton();
}
// 公有靜態方法,回傳實體物件
public static Singleton getInstance() {
return instance;
}
}
餓漢式的優點是,在類裝載的時候就完成了實體化,避免了執行緒同步問題,
但是,這樣的實作方式不支持延遲加載實體,如果從始至終未使用過這個實體,就會造成記憶體浪費,
并且,餓漢式在一些場景中無法使用:比如單例類實體的創建是依賴引數或者組態檔的,在通過 getInstance() 方法獲取實體物件之前需要呼叫某個方法設定引數給物件實體,則這種方式將無法使用,
懶漢式
懶漢式相對于餓漢式的優勢是支持延遲加載,可以在需要使用實體的時候才進行初始化,
對應的 Java 代碼片段如下:
public class Singleton {
// 靜態變數
private static Singleton instance;
// 構造器私有化
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
// 實體不存在時初始化
instance = new Singleton();
}
return instance;
}
}
上述的實作方式是執行緒不安全的,如果有兩個執行緒同時進入到 getInstance() 方法,并且正好都通過了判斷陳述句,這時便會產生多個實體,通常不建議在生產環境中使用執行緒不安全的懶漢式創建單例類,
為了做到執行緒安全,可以給 getInstance() 方法加一把鎖,
對應的 Java 代碼片段如下:
public class Singleton {
// 靜態變數
private static Singleton instance;
// 構造器私有化
private Singleton() {}
// 使用 synchronized 對方法進行加鎖
public static synchronized Singleton getInstance() {
if (instance == null) {
// 實體不存在時初始化
instance = new Singleton();
}
return instance;
}
}
上述在 getInstance() 方法加鎖的方式解決了執行緒不安全的問題,但是,由于加鎖的粒度較大,實際的效率非常低,
如果這個單例類偶爾會被使用到,那這種實作方式還可以接受,但是,如果頻繁地用到,那頻繁加鎖、釋放鎖則會出現并發度低的問題,造成性能瓶頸,
因此,也不建議在生產環境中使用執行緒安全的懶漢式創建單例類,
雙重檢測
餓漢式和懶漢式的實作方式都有一定的限制,而雙重檢測的實作方式是一種既支持延遲加載、又支持高并發的單例實作方式,
對應的 Java 代碼片段如下:
public class Singleton {
// 靜態變數
private static Singleton instance;
// 構造器私有化
private Singleton() {}
public static Singleton getInstance() {
// 一次檢測
if (instance == null) {
synchronized (Singleton.class) {
// 二次檢測
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
當有兩個執行緒同時進入到 getInstance() 方法時,雖然會出現都通過第一次檢查的判斷陳述句,但是只會有一個執行緒獲得鎖并實體化物件,即使后續再有執行緒進入到同步代碼塊中,也會被第二次檢查的判斷陳述句擋在外面,
雙重檢測方式在多執行緒開發中常使用到,其優點是執行緒安全、支持延遲加載、效率較高,在實際開發中比較推薦使用這種方式實作單例模式,
靜態內部類
靜態內部類是一種比雙重檢測更加簡單的實作方式,它有點類似餓漢式,但又能做到延遲加載,
對應的 Java 代碼片段如下:
public class Singleton {
// 靜態內部類
private static class SingletonHolder {
// 初始化實體
private static final Singleton instance = new Singleton();
}
// 構造器私有化
private Singleton() {}
public static Singleton getInstance() {
// 回傳內部類的靜態實體
return SingletonHolder.instance;
}
}
這種方式采用類裝載機制來保證初始化實體時只有一個執行緒,
靜態內部類方式在單例類被加載的時候并不會立即實體化,而是在呼叫 getInstance() 方法的時候,才會裝載 SingletonHolder 類,從而實作單例類的實體化,
類的靜態屬性只會在第一次加載類的時候初始化,實體的唯一性、創建程序的執行緒安全性,都由 JVM 來保證,
所以,這種實作方法既保證了執行緒安全,又能做到延遲加載,效率也比較高,也是一種推薦使用的實作方式,
列舉
基于列舉型別的單例實作,是最簡單的實作方式,
對應的 Java 代碼片段如下:
public enum Singleton {
// 實體屬性
INSTANCE;
public void doSomething() {
// 通過以下方式呼叫此方法
// Singleton.INSTANCE.doSomething();
}
}
這種方式是通過 Java 列舉型別本身的特性,保證了實體創建的執行緒安全性和實體的唯一性,還能防止反序列化重新創建新的物件,
這種方式是Effective Java中文版(第3版)作者提倡的方式,推薦在生產環境中使用,
深度理解
單例模式唯一性的范圍
單例類只允許創建唯一物件(或實體),這里物件的唯一性范圍指的是行程內只允許創建一個物件,
行程之間是不共享地址空間的,如果在一個行程中創建另一個行程,作業系統會給新行程分配新的地址空間,并且將老行程地址空間的所有內容重新拷貝一份到新行程的地址空間中,這些內容包括代碼、資料,
所以,單例類在老行程中存在且只能存在一個物件,在新行程中也會存在且只能存在一個物件,而且,這兩個物件不是同一個物件,
實作執行緒唯一的單例
“行程唯一”指的是行程內唯一,行程間不唯一,類比得知,“執行緒唯一”指的是執行緒內唯一,執行緒間不唯一,
其實,“行程唯一”的單例在同一個行程中的執行緒間唯一,若要做到“執行緒唯一”,主要是做到執行緒間保持不唯一,
實作執行緒唯一單例的代碼很簡單,可以通過一個鍵值對做關聯存盤,其中 key 是執行緒 ID,value 是物件,
對應的 Java 代碼片段如下:
import java.util.concurrent.ConcurrentHashMap;
public class Singleton {
// 保證執行緒唯一的鍵值對
private static final ConcurrentHashMap<Long, Singleton> instanceMap = new ConcurrentHashMap<>();
// 構造器私有化
private Singleton() {}
public static Singleton getInstance() {
Long currentThreadId = Thread.currentThread().getId();
instanceMap.putIfAbsent(currentThreadId, new Singleton());
return instanceMap.get(currentThreadId);
}
}
實作集群唯一的單例
這里的集群表示行程集群,類比可知,“集群唯一”相當于行程間也唯一,即在不同的行程間共享同一個物件,不創建同一個類的多個物件,
實作集群唯一單例需要依賴到外部共享存盤區:將單例物件序列化并存盤到外部共享存盤區,在使用這個單例物件的時候,需要先從外部共享存盤區中將它讀取到記憶體,并反序列化成物件,然后再使用,使用完成之后還需要再存盤回外部共享存盤區,
為了保證任何時刻在集群中都只有一份物件存在,一個行程在獲取到物件之后,需要對物件加鎖,避免其他行程再將其獲取,
在行程使用完這個物件之后,還需要顯式地將物件從記憶體中洗掉,并且釋放物件的鎖,
實作一個多例模式
“多例”指的是,一個類可以創建多個物件,但是個數是有限制的,同無限個有一些區別,
多例模式的實作也比較簡單,通過一個鍵值對存盤索引和物件之間的對應關系,并且需要控制物件的個數,
對應的 Java 代碼片段如下:
import java.util.Map;
import java.util.HashMap;
import java.util.Random;
public class Multipleton {
// 限制實體數量
private static final int COUNT = 3;
// 存盤對應關系的鍵值對
private static final Map<Integer, Multipleton> instanceMap = new HashMap<>();
// 餓漢式實作
static {
instanceMap.put(0, new Multipleton());
instanceMap.put(1, new Multipleton());
instanceMap.put(2, new Multipleton());
}
// 構造器私有化
private Multipleton() {}
// 公有靜態方法,回傳對應索引的實體物件
public static Multipleton getInstance(Integer index) {
return instanceMap.get(index);
}
// 公有靜態方法,回傳隨機索引的實體物件
public static Multipleton getRandomInstance() {
Random random = new Random();
Integer index = random.nextInt(COUNT);
return instanceMap.get(index);
}
}
總結
優點
單例模式的主要優點如下:
- 提供了對唯一實體的受控訪問,封裝性非常好
- 系統記憶體中只存在一個物件,可以節省系統資源
- 基于單例模式,可擴展實作多例類,既節省系統資源,又解決了由于單例模式共享過多有損性能的問題
缺點
單例模式的主要缺點如下:
- 單例模式對面向物件特性的支持不友好,違背了基于介面而非實作的設計原則
- 單例模式對代碼的擴展性不友好,如要擴展則會導致改動較大
- 常規的單例模式不支持有引數的建構式,只能通過其他方式改動單例類中的成員變數
- 對于有 GC 的編程語言,如果長時間不使用實體化的物件,則單例物件有可能會被銷毀
適用場景
單例模式的適用場景如下:
- 單例模式主要針對需要頻繁地創建和銷毀的物件,可以理解成創建物件時耗時過多或耗費資源較大但又經常用到的物件,如工具類物件、頻繁訪問的資料庫或檔案物件
- 從業務概念上看,有些資料在系統中只應該保存一份,就比較適合設計成單例類,比如,系統的配置資訊類
- 可以使用單例解決資源訪問沖突的問題,單例模式可以只提供一個公共訪問點
原始碼
在 JDK 中,java.lang.Runtime 是經典的單例模式,其用于與 Java 運行時環境進行互動,
Runtime 類是一個典型的餓漢式單例模式實作,如下是其的一些實作邏輯:
public class Runtime {
// 靜態實體化
private static final Runtime currentRuntime = new Runtime();
private static Version version;
// 靜態方法獲取靜態實體
public static Runtime getRuntime() {
return currentRuntime;
}
// 構造器私有化
private Runtime() {}
首發于翔仔的個人博客,點擊查看更多,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/502767.html
標籤:其他
