天天看點

單例模式(最簡單&&常問)——Java實作

目的:

確定一個類隻有一個執行個體,并提供該執行個體的全局通路點。

原理:

使用一個私有構造函數、一個私有靜态變量以及一個公有靜态函數來實作。

私有構造函數保證了不能通過構造函數來建立對象執行個體,隻能通過公有靜态函數傳回唯一的私有靜态變量。

個人通俗解釋:

核反應堆控制,線程池之類資源管理相關。

是以特别需要考慮線程安全性。

實作方案:

  1. 懶漢式-線程不安全

以下實作中,私有靜态變量 uniqueInstance 被延遲執行個體化,這樣做的好處是,如果沒有用到該類,那麼就不會執行個體化 uniqueInstance,進而節約資源。

這個實作在多線程環境下是不安全的,如果多個線程能夠同時進入 if (uniqueInstance == null) ,并且此時 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;
    }
}
           
  1. 餓漢式-線程安全

線程不安全問題主要是由于 uniqueInstance 被執行個體化多次,采取直接執行個體化 uniqueInstance 的方式就不會産生線程不安全問題。

但是直接執行個體化的方式也丢失了延遲執行個體化帶來的節約資源的好處。

  1. 懶漢式-線程安全

隻需要對 getUniqueInstance() 方法加鎖,那麼在一個時間點隻能有一個線程能夠進入該方法,進而避免了執行個體化多次 uniqueInstance。

但是當一個線程進入該方法之後,其它試圖進入該方法的線程都必須等待,即使 uniqueInstance 已經被執行個體化了。這會讓線程阻塞時間過長,是以該方法有性能問題,不推薦使用。

public static synchronized Singleton getUniqueInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}
           
  1. 雙重校驗鎖-線程安全

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 的指令重排,保證在多線程環境下也能正常運作。

  1. 靜态内部類實作

    當 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;
    }
}
           

總結

4和5 最佳方案

1 懶人方案

2 浪費資源,最懶人方案

3 傻逼方案