單例(Singleton)
1. 單例(Singleton)
Intent
確保一個類只有一個例項,並提供該例項的全域性訪問點。
Class Diagram
使用一個私有建構函式、一個私有靜態變數以及一個公有靜態函式來實現。
私有建構函式保證了不能通過建構函式來建立物件例項,只能通過公有靜態函式返回唯一的私有靜態變數。
Implementation
Ⅰ 懶漢式-執行緒不安全
以下實現中,私有靜態變數 uniqueInstance 被延遲例項化,這樣做的好處是,如果沒有用到該類,那麼就不會例項化 uniqueInstance,從而節約資源。
這個實現在多執行緒環境下是不安全的,如果多個執行緒能夠同時進入 if (uniqueInstance == null)
uniqueInstance = new Singleton();
語句,這將導致例項化多次 uniqueInstance。
public class Singleton { private static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } }
Ⅱ 餓漢式-執行緒安全
執行緒不安全問題主要是由於 uniqueInstance 被例項化多次,採取直接例項化 uniqueInstance 的方式就不會產生執行緒不安全問題。
但是直接例項化的方式也丟失了延遲例項化帶來的節約資源的好處。
private static Singleton uniqueInstance = new Singleton();
Ⅲ 懶漢式-執行緒安全
只需要對 getUniqueInstance() 方法加鎖,那麼在一個時間點只能有一個執行緒能夠進入該方法,從而避免了例項化多次 uniqueInstance。
但是當一個執行緒進入該方法之後,其它試圖進入該方法的執行緒都必須等待,即使 uniqueInstance 已經被例項化了。這會讓執行緒阻塞時間過長,因此該方法有效能問題,不推薦使用。
public static synchronized Singleton getUniqueInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; }
Ⅳ 雙重校驗鎖-執行緒安全
uniqueInstance 只需要被例項化一次,之後就可以直接使用了。加鎖操作只需要對例項化那部分的程式碼進行,只有當 uniqueInstance 沒有被例項化時,才需要進行加鎖。
雙重校驗鎖先判斷 uniqueInstance 是否已經被例項化,如果沒有被例項化,那麼才對例項化語句進行加鎖。
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { if (uniqueInstance == null) { synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
考慮下面的實現,也就是隻使用了一個 if 語句。在 uniqueInstance == null 的情況下,如果兩個執行緒都執行了 if 語句,那麼兩個執行緒都會進入 if 語句塊內。雖然在 if 語句塊內有加鎖操作,但是兩個執行緒都會執行 uniqueInstance = new Singleton();
這條語句,只是先後的問題,那麼就會進行兩次例項化。因此必須使用雙重校驗鎖,也就是需要使用兩個 if 語句。
if (uniqueInstance == null) { synchronized (Singleton.class) { uniqueInstance = new Singleton(); } }
uniqueInstance 採用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton();
這段程式碼其實是分為三步執行:
- 為 uniqueInstance 分配記憶體空間
- 初始化 uniqueInstance
- 將 uniqueInstance 指向分配的記憶體地址
但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1>3>2。指令重排在單執行緒環境下不會出現問題,但是在多執行緒環境下會導致一個執行緒獲得還沒有初始化的例項。例如,執行緒 T1 執行了 1 和 3,此時 T2 呼叫 getUniqueInstance() 後發現 uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保證在多執行緒環境下也能正常執行。
Ⅴ 靜態內部類實現
當 Singleton 類載入時,靜態內部類 SingletonHolder 沒有被載入進記憶體。只有當呼叫 getUniqueInstance()
方法從而觸發 SingletonHolder.INSTANCE
時 SingletonHolder 才會被載入,此時初始化 INSTANCE 例項,並且 JVM 能確保 INSTANCE 只被例項化一次。
這種方式不僅具有延遲初始化的好處,而且由 JVM 提供了對執行緒安全的支援。
public class Singleton { private Singleton() { } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getUniqueInstance() { return SingletonHolder.INSTANCE; } }
Ⅵ 列舉實現
public enum Singleton { INSTANCE; private String objName; public String getObjName() { return objName; } public void setObjName(String objName) { this.objName = objName; } public static void main(String[] args) { // 單例測試 Singleton firstSingleton = Singleton.INSTANCE; firstSingleton.setObjName("firstName"); System.out.println(firstSingleton.getObjName()); Singleton secondSingleton = Singleton.INSTANCE; secondSingleton.setObjName("secondName"); System.out.println(firstSingleton.getObjName()); System.out.println(secondSingleton.getObjName()); // 反射獲取例項測試 try { Singleton[] enumConstants = Singleton.class.getEnumConstants(); for (Singleton enumConstant : enumConstants) { System.out.println(enumConstant.getObjName()); } } catch (Exception e) { e.printStackTrace(); } } }
firstName secondName secondName secondName
該實現在多次序列化再進行反序列化之後,不會得到多個例項。而其它實現需要使用 transient 修飾所有欄位,並且實現序列化和反序列化的方法。
該實現可以防止反射攻擊。在其它實現中,通過 setAccessible() 方法可以將私有建構函式的訪問級別設定為 public,然後呼叫建構函式從而例項化物件,如果要防止這種攻擊,需要在建構函式中新增防止多次例項化的程式碼。該實現是由 JVM 保證只會例項化一次,因此不會出現上述的反射攻擊。
Examples
- Logger Classes
- Configuration Classes
- Accesing resources in shared mode
- Factories implemented as Singletons