詳談單例、餓漢、和懶漢模式

1、基本概念

單例模式屬於建立型設計模式。設計模式

確保一個類只有一個實例,並提供該實例的全局訪問點。安全

實現: 使用一個私有構造函數、一個私有靜態變量以及一個公有靜態函數來實現bash

2、結構

類圖:多線程



私有構造函數保證了不能經過構造函數來建立對象實例,只能經過公有靜態函數返回惟一的私有靜態變量併發

3、幾類經典單例模式實現

一、懶漢式-線程不安全

下面的實現中,私有靜態變量 uniqueInstance 被延遲實例化,這樣作的好處是,若是沒有用到該類,那麼就不會實例化 uniqueInstance,從而節約資源。jvm

這個實如今多線程環境下是不安全的,若是多個線程可以同時進入 if (uniqueInstance == null) ,而且此時 uniqueInstance == null,那麼會有多個線程執行 uniqueInstance = new Singleton(); 語句,這將致使實例化屢次 uniqueInstance函數

// 懶漢式: 線程不安全
// 有延遲加載: 不是在類加載的時候就建立了,而是在調用newStance()的時候纔會建立
public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton(){

    }

    public static Singleton newInstance(){
        if(uniqueInstance == null)
            uniqueInstance = new Singleton();
        return uniqueInstance;
    }
}複製代碼

二、懶漢式-線程安全-性能很差

爲了解決上面的問題,咱們能夠直接在newInstance()方法上面直接加上一把synchronized同步鎖。那麼在一個時間點只能有一個線程可以進入該方法,從而避免了實例化屢次 uniqueInstance性能

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

public static synchronized Singleton newInstance(){//在上面的基礎上加了synchronized
    if(uniqueInstance == null)
        uniqueInstance = new Singleton();
    return uniqueInstance;
}複製代碼

三、餓漢式-線程安全-無延遲加載

餓漢式就是 : 採起直接實例化 uniqueInstance 的方式,這樣就不會產生線程不安全問題。ui

這種方式比較經常使用,但容易產生垃圾對象(丟失了延遲實例化(lazy loading)帶來的節約資源的好處)。

它基於 classloader機制避免了多線程的同步問題,不過,instance 在類裝載時就實例化,雖然致使類裝載的緣由有不少種,在單例模式中大多數都是調用 getInstance 方法, 可是也不能肯定有其餘的方式(或者其餘的靜態方法)致使類裝載,這時候初始化 instance 顯然沒有達到 lazyloading 的效果

public class Singleton {

    // 急切的建立了uniqueInstance, 因此叫餓漢式
    private static Singleton uniqueInstance = new Singleton();

    private Singleton(){
    }

    public static Singleton newInstance(){
        return uniqueInstance;
    }

    // 瞎寫一個靜態方法。這裏想說的是,若是咱們只是要調用 Singleton.getStr(...),
    // 原本是不想要生成 Singleton 實例的,不過沒辦法,已經生成了
    public static String getStr(String str) {return "hello" + str;}
}複製代碼

四、雙重校驗鎖-線程安全

uniqueInstance 只須要被實例化一次,以後就能夠直接使用了。加鎖操做只須要對實例化那部分的代碼進行,只有當uniqueInstance 沒有被實例化時,才須要進行加鎖。

雙重校驗鎖先判斷 uniqueInstance 是否已經被實例化,若是沒有被實例化,那麼纔對實例化語句進行加鎖

// 雙重加鎖
public class Singleton {

    // 和餓漢模式相比,這邊不須要先實例化出來
    // 注意這裏的 volatile,使用 volatile 能夠禁止 JVM 的指令重排,保證在多線程環境下也能正常運行
    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton newInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                // 這一次判斷也是必須的,否則會有併發問題
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}複製代碼
注意,內層的第二次 if (uniqueInstance == null) {也是必須的,若是不加: 也就是隻使用了一個 if 語句。在 uniqueInstance == null 的狀況下,若是兩個線程都執行了 if 語句,那麼兩個線程都會進入 if 語句塊內。雖然在 if 語句塊內有加鎖操做,可是兩個線程都會執行 uniqueInstance = new Singleton();這條語句,只是前後的問題,那麼就會進行兩次實例化。所以必須使用雙重校驗鎖,也就是須要使用兩個 if 語句。

volatile 關鍵字修飾也是頗有必要的, uniqueInstance = new Singleton(); 這段代碼實際上是分爲三步執行:

  • 1)、爲 uniqueInstance 分配內存空間;
  • 2)、初始化 uniqueInstance
  • 3)、將 uniqueInstance 指向分配的內存地址;

可是因爲 JVM 具備指令重排的特性,執行順序有可能變成 1>3>2。指令重排在單線程環境下不會出現問題,可是在多線程環境下會致使一個線程得到尚未初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 newInstance() 後發現 uniqueInstance 不爲空,所以返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。

使用 volatile 能夠禁止 JVM 的指令重排,保證在多線程環境下也能正常運行

五、靜態內部類實現

Singleton 類加載時,靜態內部類 Holder 沒有被加載進內存。只有當調用 newInstance()方法從而觸發 Holder.uniqueInstanceHolder 纔會被加載,此時初始化 uniqueInstance實例,而且 JVM 能確保 uniqueInstance只被實例化一次。

這種方式不只具備延遲初始化的好處,並且由 JVM 提供了對線程安全的支持。

這種方式是 Singleton 類被裝載了, uniqueInstance 不必定被初始化。由於 Holders 類沒有被主動使用,只有經過顯式調用 newInstance() 方法時,纔會顯式裝載 Holder 類,從而實例化 uniqueInstance
public class Singleton {

    private Singleton() {
    }

    // 主要是使用了 嵌套類能夠訪問外部類的靜態屬性和靜態方法 的特性
    // 不少人都會把這個嵌套類說成是靜態內部類,嚴格地說,內部類和嵌套類是不同的,它們能訪問的外部類權限也是不同的。
    private static class Holder {
        private static final Singleton uniqueInstance = new Singleton();
    }
    public static Singleton newInstance() {
        return Holder.uniqueInstance;
    }
}複製代碼

六、枚舉實現

這種實現方式尚未被普遍採用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止屢次實例化。

該實如今屢次序列化再進行反序列化以後,不會獲得多個實例。而其它實現須要使用 transient修飾全部字段,而且實現序列化和反序列化的方法。

枚舉實現單例 (+測試):

public class Singleton {

    private Singleton() {

    }

    public static Singleton newInstance() {
        return Sing.INSTANCE.newInstance();
    }

    private enum Sing {

        INSTANCE;

        private Singleton singleton;

        //jvm guarantee only run once
        Sing() {
            singleton = new Singleton();
        }

        public Singleton newInstance() {
            return singleton;
        }
    }

    public static int clientTotal = 1000;

    public static int threadTotal = 200;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();

        Semaphore semaphore = new Semaphore(threadTotal);
        CountDownLatch countDownLatch = new CountDownLatch(clientTotal);

        Set<Singleton>set = Collections.synchronizedSet(new HashSet<>());//注意set也要加鎖

        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();

                    set.add(Singleton.newInstance());

                    semaphore.release();
                } catch (Exception e) {

                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();

        System.out.println(set.size());//1
    }
}複製代碼

關於序列化和反序列化:

public enum Singleton {

    INSTANCE;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}複製代碼

測試:

public class Test {

    public static void main(String[] args){
        // 單例測試
        Singleton s1 = Singleton.INSTANCE;
        s1.setName("firstName");
        System.out.println("s1.getName(): " + s1.getName());

        Singleton s2 = Singleton.INSTANCE;
        s2.setName("secondName");

        //注意我這裏輸出s1 ,可是已經變成了 secondName
        System.out.println("s1.getName(): " + s1.getName());
        System.out.println("s2.getName(): " + s2.getName());

        System.out.println("-----------------");

        // 反射獲取實例測試
        Singleton[] enumConstants = Singleton.class.getEnumConstants();
        for (Singleton enumConstant : enumConstants)
            System.out.println(enumConstant.getName());
    }
}複製代碼

輸出:

s1.getName(): firstName
s1.getName(): secondName
s2.getName(): secondName
-----------------
secondName複製代碼
該實現能夠防止反射攻擊。在其它實現中,經過 setAccessible()(反射中的強制訪問私有屬性方法) 方法能夠將私有構造函數的訪問級別設置爲 public,而後調用構造函數從而實例化對象,若是要防止這種攻擊,須要在構造函數中添加防止屢次實例化的代碼。該實現是由 JVM 保證只會實例化一次,所以不會出現上述的反射攻擊。

4、總結

通常狀況下,不建議使用懶漢方式,建議使用餓漢方式。

只有在要明確實現 lazy loading 效果時,纔會使用靜態內部類方式

若是涉及到反序列化建立對象時,能夠嘗試使用枚舉方式。

若是有其餘特殊的需求,能夠考慮使用雙檢鎖方式。

相關文章
相關標籤/搜索