單例模式的Java實(shí)現(xiàn)與思考

注:本文來源于網(wǎng)絡(luò)

一、摘要

本文對(duì)單例模式在 Java 里的不同實(shí)現(xiàn)方式進(jìn)行了分析,對(duì)比了不同方案的優(yōu)缺點(diǎn)并給出使用結(jié)論。

二、單例模式的實(shí)現(xiàn)

單例模式的要義是控制某一個(gè)類只有一個(gè)唯一的實(shí)例,并提供一個(gè)統(tǒng)一的訪問點(diǎn)。它主要用在某些不希望有多個(gè)實(shí)例的場景,比如線程池。

1.基礎(chǔ)實(shí)現(xiàn)

首先來看一個(gè)單例模式的基礎(chǔ)實(shí)現(xiàn):

public class Singleton {

    private static final Singleton instance = new Singleton();

    private Singleton() {
        System.out.println("Singleton()");
    }

    public static Singleton getInstance() {
        return instance;
    }
}

這個(gè)實(shí)現(xiàn)聲明了一個(gè)靜態(tài)變量 instance,利用了 Java 類初始化的規(guī)則(詳見下方參考資料里的 JSL 12.4.1):

  • 一個(gè)類只有在被實(shí)例化,調(diào)用靜態(tài)方法和訪問靜態(tài)變量之前才會(huì)被初始化。
  • 類的初始化會(huì)執(zhí)行靜態(tài)初始化塊和靜態(tài)變量的初始化語句。
  • 類的初始化過程會(huì)加鎖,可以保證只會(huì)執(zhí)行一次。

因此這種方式保證了 instance 只會(huì)在類初始化的時(shí)候被創(chuàng)建一次,之后每次通過 getInstance 獲取到的都是同一個(gè)實(shí)例。

2.懶加載實(shí)現(xiàn)

有時(shí)不想要實(shí)例過早初始化,而是在真正使用到的時(shí)候才初始化,這種策略被稱為懶加載。

2.1 基礎(chǔ)實(shí)現(xiàn)
public class LazySingletonNaive {

    private static LazySingletonNaive instance;

    private LazySingletonNaive() {
        System.out.println("LazySingletonNaive()");
    }

    public static LazySingletonNaive getInstance() {
        if (instance == null) {
            instance = new LazySingletonNaive();
        }
        return instance;
    }
}

注意這里的 getInstance 方法存在競態(tài)條件,有可能兩個(gè)線程分別檢查到 instance == null,然后各自執(zhí)行了一次實(shí)例化操作。

而且由于指令重排的原因,還有可能造成部分初始化問題,這一點(diǎn)在后面 DCL 時(shí)會(huì)說到。

2.2 線程安全的懶加載
  • 方法1:直接對(duì) getInstance 方法加 synchronized 鎖
public static synchronized LazySingletonNaive getInstance() {

這種方式最簡單,但會(huì)帶來同步性能開銷,僅在應(yīng)用可以接受這種性能開銷時(shí)使用。

  • 方法2:Double Check Lock
public class LazySingletonDCL {

    private static volatile LazySingletonDCL instance; // 這里必須使用 volatile 關(guān)鍵字,是為了避免部分初始化問題,下文詳述。

    private LazySingletonDCL() {
        System.out.println("LazySingletonDCL()");
    }

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

}

getInstance 方法的邏輯是:

  1. 首先判斷 instance 是否為空,如果不為空,則說明已經(jīng)初始化過了,可以直接使用,這種情況下可以不用進(jìn)入同步塊,避免了絕大多數(shù)情況下的性能開銷;
  2. 如果為空,則說明需要進(jìn)行初始化,進(jìn)入同步塊,拿到鎖之后需要再檢查一次 instance 是否為空(Double Check 名稱的來源),如果 instance 為空,則執(zhí)行初始化,否則說明其他線程已經(jīng)初始化過了,直接返回即可。

instance 實(shí)例必須要使用 volatile 修飾,是為了避免部分初始化問題。

  • 方法3:Holder 方式
public class LazySingletonHolder {

    private static class Holder {
        static final LazySingletonHolder instance = new LazySingletonHolder();
    }

    private LazySingletonHolder() {
        System.out.println("LazySingletonHolder()");
    }

    public static LazySingletonHolder getInstance() {
        return Holder.instance;
    }

}

這里新創(chuàng)建了一個(gè)靜態(tài)私有內(nèi)部類 Holder,它的唯一目的就是存儲(chǔ)一個(gè)靜態(tài)的 instance 實(shí)例。然后這個(gè)類只有在 getInstance 方法調(diào)用時(shí)才會(huì)初始化,從而把 instance 實(shí)例初始化,實(shí)現(xiàn)了懶加載。

這種方式也比較簡單,同時(shí)避免了同步的性能開銷,推薦使用。

問:同樣是利用類的靜態(tài)變量,為什么說 Singleton 和 LazySingletonHolder 一個(gè)沒有懶加載,一個(gè)實(shí)現(xiàn)了懶加載呢?

答:如果按照上面貼出來的代碼,Singleton 類的初始化時(shí)機(jī)只有一個(gè),那就是 getInstance 方法調(diào)用時(shí),那它其實(shí)就是懶加載。但是實(shí)際中,這個(gè)類里可能不止 getInstance 一個(gè)公開方法,如果還暴露了其他的公開方法或者變量,在訪問那些方法或變量時(shí),也會(huì)觸發(fā)類的初始化,就會(huì)造成還沒調(diào)用 getInstance 就初始化了實(shí)例的情況,所以說它沒有實(shí)現(xiàn)懶加載。 而 LazySingletonHolder 類里的 Holder 由于被限制為了私有,且靜態(tài)變量的唯一訪問時(shí)機(jī)就是 getInstance 方法,因此可以實(shí)現(xiàn)懶加載。 所以這兩個(gè)類的核心區(qū)別在于 instance 的初始化時(shí)機(jī),Singleton 沒有做嚴(yán)格管控,有可能會(huì)被提前初始化,LazySingletonHolder 做了嚴(yán)格管控,從而實(shí)現(xiàn)了懶加載。

三、幾種方式的對(duì)比與總結(jié)

結(jié)論:

基礎(chǔ)實(shí)現(xiàn)方式足夠簡單,易于理解,也沒有并發(fā)問題,如果沒有特殊的需求,建議90%的場景直接使用。
如果應(yīng)用場景確實(shí)需要懶加載,建議使用 Holder 方式。
DCL 方式有點(diǎn) tricky,可讀性不夠好,而且相比 Hold 方式?jīng)]有優(yōu)勢,因此不建議使用。

那是不是 DCL 方式就沒有用處了呢?也不是,如果想要對(duì)一個(gè)實(shí)例變量做懶加載,基于靜態(tài)變量的 Holder 方式就行不通了,此時(shí)就必須使用 DCL 才行了。

以上就是單例模式的集體內(nèi)容,以下就是單例模式的相關(guān)面試資料與答案

單例模式面試題與答案,獲取方式

請加QQ群:976203838

獲取以上面試題答案傳送門:https://shimo.im/docs/BYMjlkYOqM08diLI

重要的話說三遍,先 關(guān)注關(guān)注關(guān)注,然后加群才可拿到參考答案哦!
image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。