[Java] 細數幾種單例的實現方式

前言


單例模式是比較常見且常用的設計模式之一,Java中有諸多種單例設計模式的實現。現在筆者將在本文盤點在Java中實現單例模式的幾種常見的方法。

參考


  1. Atomic Access - oracle docs
  2. 《Effective Java 3rd edition》 - Item 3: Enforce the singleton property with a private constructor or an enum type
  3. Serialization of Enum Constants - oracle docs

什麼是單例模式?


在介紹幾種單例模式的實現之前,筆者想先澄清一下單例模式的定義。

單例模式是一種設計模式,也是最爲簡單的設計模式。在OOP裏,當我們在邏輯上認爲某類應該只有一個實例的時候,我們採用此設計模式,下面筆者簡單列舉一下我們也許能用到單例模式的地方(※非必須)。

  • Logger類:統一寫log的入口。
  • 應用Configuration類:你的應用程序不會想要多個存放配置信息的配置類實例。
  • 保存數據的倉庫類:如數據生產者提供的消息(數據),保存在消息倉庫裏。這時你不應該提供多個倉庫類的實例,因爲這些數據應該被放在同一個倉庫裏。

JDK裏的單例類


筆者在本節列舉了一些在JDK裏使用單例模式的類以供參考。有興趣的讀者可以自行在開源JDK裏尋找其對應源碼,參考其單例實現。

  • java.lang.Runtime
  • java.awt.Desktop
  • java.lang.SecurityManager.java

單例模式的實現


單例模式在java中有諸多實現,各類實現也有各自不同的優缺點。
這裏的特點主要有

  • 懶漢實現與非懶漢
  • 線程安全與非線程安全
  • 併發高性能與併發低性能
  • 序列化・反序列化的安全與否
  • 是否抗反射攻擊

筆者在下表列出幾種單例模式的基本信息。並將在後面的子章節說明每一種實現的特點並提供其示例代碼。

No. 實現 EN關鍵詞 線程安全 Lazy-Loading 高併發性能 序列化・反序列化安全性 抗反射攻擊(※1)
1 非懶漢實現 Eager initialization
2 懶漢實現 Lazy initialization
3 同步懶漢實現 Synchronized Lazy initialization
4 雙重檢查鎖定 Double checked locking
5 靜態內部類實現 Bill Pugh Singleton Implementation
6 枚舉實現 Enum Singleton

※1. 抗反射攻擊:對Java來說,私有化構造器只能放置在編譯時,被外部調用,卻並不能阻止通過反射(reflection)API AccessibleObject.setAccessible 來在運行時改變其訪問控制。

1. 非懶漢實現 (Eager initialization)


筆者第一次看到Eager的時候也是很懵,不知道如何翻譯。有人說Eager理解爲Preloading,筆者覺得這個替換不錯。Preloading是預先加載,也就是本節標題的非懶漢實現了。
也有別的地方稱這實現爲餓漢實現

其具體實現如下:

/**
 * 利用靜態初始化在class load階段初始化單例實例。
 */
public class EagerInstantiationSingleton {

    /**
     * 私有類成員變量,用於存放唯一實例。
     */
    private static EagerInstantiationSingleton sharedInstance = new EagerInstantiationSingleton();

    /**
     * 構造器私有化。
     */
    private EagerInstantiationSingleton() {}

    /**
     * 公有類方法,獲取唯一實例。
     */
    public static EagerInstantiationSingleton getInstance() {
        return sharedInstance;
    }
}

這種實現會在類加載的時候,利用靜態初始化初始化其唯一實例。這種實現非常簡單,當你的單例類不佔用大量資源的時候,可以使用這種簡單的單例實現方式。

而當你的單例類佔用大量計算機資源的時候,你就需要考慮使用懶漢實現,來延遲其初始化的時間。

2. 懶漢實現 (Lazy initialization)


經典的不加鎖的懶漢實現,與上述的非懶漢實現相比起來,這種實現有延遲加載的特點,可以延遲實例的初始化直到client(調用方)調用getInstance()方法時才初始化。但因不加鎖,在高併發的情況下,很可能會出現被多次實例化的情況(也就是所謂的線程不安全)。

在多線程程序裏,並不推薦使用這種實現,但當你的程序只有單線程時,請放心大膽使用。

其具體實現如下:

/**
 * 經典單例實現
 */
public class ClassicSingleton {

    /**
     * 私有類成員變量,用於存放唯一實例。
     */
    private static ClassicSingleton sharedInstance;

    /**
     * 構造器私有化。
     */
    private ClassicSingleton() {}

    /**
     * 公有類方法,獲取唯一實例。
     */
    public static ClassicSingleton getInstance() {
        if (sharedInstance == null)
            sharedInstance = new ClassicSingleton();
        return sharedInstance;
    }
}

3. 同步懶漢實現(Synchronized Lazy initialization)


上面在懶漢實現一節,我們提到了懶漢實現並不是線程安全的,
爲了解決線程不安全的問題,有了第一次嘗試改進(即:並非最終解決方案),這便是本實現。

這個實現雖然解決了線程安全的問題,但卻引入了新的問題,那就是因同步塊作用域範圍過大,導致性能急劇下降,在高併發場景,這顯然是不可取的,所以作爲一種不上不下的方案,讀者們只要知道有這種實現即可。

其具體實現如下:

/**
 * 利用synchronized同步關鍵字實現,
 * 線程安全的傳統單例。
 */
public class ThreadSafeClassicSingleton {
    /**
     * 私有類成員變量,用於存放唯一實例。
     */
    private static ThreadSafeClassicSingleton sharedInstance;

    /**
     * 構造器私有化。
     */
    private ThreadSafeClassicSingleton() {}

    /**
     * 同步公有類方法,獲取唯一實例。
     */
    public static synchronized ThreadSafeClassicSingleton getInstance() {
        if (sharedInstance == null)
            sharedInstance = new ThreadSafeClassicSingleton();
        return sharedInstance;
    }
}

如果你不知道什麼叫同步塊作用域過大,請看
If i synchronized two methods on the same class, can they run simultaneously? - Stack overflow
靜態同步方法與實例同步方法類似,使用的鎖對象是其Class類的實例(ThreadSafeClassicSingleton.class)。其作用範圍是該類所有的同步類方法

4. 雙重檢查鎖定實現 (Double checked locking / DCL)


在上一節,我們提到了同步懶漢實現的問題

“因同步塊作用域範圍過大,導致性能急劇下降”

而雙重檢查鎖定實現則解決這一問題。通過縮小同步塊作用範圍,讓同步塊僅作用於創建實例的部分,而不是整個方法。

  1. 第一重檢查很好的規避了初始化成功後的搶鎖操作,提高了性能。

  2. 第二重檢查則是爲了讓剛開始未初始化成功時,避免同時搶鎖的多個線程多次創建實例,這也許難以理解,第一個線程執行結束之後,其他等待鎖的線程會依次進入該同步塊,所以第二重檢查是爲了阻止除第一個被執行的線程之外的線程創建實例。

  3. volatile關鍵字,是爲了保證讓其他線程對sharedInstance被寫入操作的可見性。即當一個線程寫入數據到sharedInstance時,其他線程能直接獲取到最新的sharedInstance的值,而非緩存的值。

這種實現是面試中常常被問到的一種實現。

具體實現如下:

/**
 * 縮小同步塊影響範圍的高速線程安全的懶漢單例實現
 */
public class DoubleCheckedLockingSingleton {
    /**
     * 私有類成員變量,用於存放唯一實例。
     * volatile是關鍵,保證了高併發時,其他線程對初始化線程寫入操作的可見性。
     * Ref - "... This means that changes to a volatile variable are always visible to other threads."
     * https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html
     */
    private volatile static DoubleCheckedLockingSingleton sharedInstance;

    /**
     * 構造器私有化。
     */
    private DoubleCheckedLockingSingleton() {}

    public DoubleCheckedLockingSingleton getInstance() {
        // 一重檢查:除了最開始同時搶鎖的那些線程之外,後續進入到本方法的線程都會被第一次檢查擋住,直接返回現有實例。
        if (sharedInstance == null) {
            // 搶鎖,高併發時,同時會有多個線程會同時搶class對象的鎖。只有一個線程能搶到鎖並開始初始化。
            synchronized (DoubleCheckedLockingSingleton.class) {
                // 二重檢查,當一個線程完成初始化操作之後,退出同步塊,釋放class的重入鎖。剩餘等待鎖的線程便能一個一個進入
                // 到這個同步塊,並由第二次檢查,放置這些線程再次初始化。
                if (sharedInstance == null)
                    sharedInstance = new DoubleCheckedLockingSingleton();
            }
        }

        return sharedInstance;
    }

}

5. 靜態內部類實現 (Bill Pugh Singleton Implementation)


在老版Java裏,因其內存模型(memory model)有諸多問題,雖然這些個問題通過JSR-133得以修正成我們現在所熟知的新版內存模型上,但在那之前,有一個叫Bill Pugh這個人想出了一種利用JVM類加載處理是線程安全的這一特性,來回避內存模型問題的實現。

這個實現利用了類加載機制,來保證線程安全性。還利用類延遲加載的特性,延遲加載內部helper類(也叫lazy holder類),實現了懶漢初始化。

這個筆者覺得非常巧妙,只不過會多生成一個class文件。但可以說這個實現非常簡單易懂,相比起DCL實現而言。

其具體實現如下:


public class BillPughSingleton {

    /**
     * 構造器私有化。
     */
    private BillPughSingleton() {}

    /**
     * 內部靜態helper類 (inner static helper class)
     */
    private static class SingletonHelper {
        private static final BillPughSingleton SHARED_INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.SHARED_INSTANCE; // 只有第一次訪問getInstance纔回去加載SingletonHelper類,高併發時
    }
}

6. 枚舉實現 (Enum Singleton)


枚舉實現,利用Java語言Enum類型抗反射和JVM類加載是線程安全的這兩個特性,使得其永遠只可能有一個實例並且是懶漢加載的(只有當第一次被使用纔會加載)。

Java編譯器禁止了對Enum類型的反射,也使其具有了抗反射攻擊的特性。

Enum類型本身也因其特殊的序列化和反序列化的過程,能直接解決序列化・反序列化造成的多個實例被創建的問題。

這種實現,非常地簡單,易懂易實現,這大概也是《Effective Java》的作者非常推崇這種寫法的原因之一吧。

延伸閱讀:Serialization of Enum Constants - oracle docs

其具體實現如下:

public enum EnumSingleTon {
    SHARED_INSTANCE;

    public static void yourMethod() {
        // 做任何你想幹的事情。
    }
}

這種實現被《Effective Java》的作者認爲是目前最好的Singleton的實現。

This approach may feel a bit unnatural, but a single-element enum type is often the best way to implement a singleton.
Note that you can’t use this approach if your singleton must extend a superclass other than Enum.

結語


雖然單例模式作爲一個簡單的設計模式,卻也有許多種實現,筆者認爲每一種實現都有其存在的道理,也有其解決的問題。希望本文能幫你理清各個實現和其之間的優化關係。

而在如Spring等DI框架大行其道的時候,我們系統的各個組件(模塊),則可以由這些DI框架來保證在運行期間僅被創建一個實例,不過這就是另一個話題了。


因篇幅和這兩種case實屬罕見的原因,筆者沒有在本文寫下關於以下兩個問題的解決方案。

  • 如何使你的Singleton實現具有序列化・反序列化安全性?
  • 如何使你的Singleton實現能抗反射攻擊?

如果有興趣的讀者可以自行查閱。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章