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

1、基本概念

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

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

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

2、結構

類圖:併發

 

 

 

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

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

一、懶漢式-線程不安全

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

這個實如今多線程環境下是不安全的,若是多個線程可以同時進入 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 的方式,這樣就不會產生線程不安全問題。測試

這種方式比較經常使用,但容易產生垃圾對象(丟失了延遲實例化(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.uniqueInstance 時 Holder 纔會被加載,此時初始化 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 效果時,纔會使用靜態內部類方式。

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

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

 

免費Java高級資料須要本身領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G。
傳送門:
相關文章
相關標籤/搜索