深刻理解單例模式

單例模式有八種寫法,不是說設計模式是表明了最佳的實踐嗎,這一下冒出八種寫法,何談最佳?java

每一種單例的寫法基本上均可以破壞其單例的屬性,這就帶來了安全隱患,因此每一種寫法都是在以前的基礎上進行增強,可是比消此漲,這會增長空間複雜度或者時間複雜度,沒有最優的用法,只有最合適的用法。設計模式

這八種裏面有些方法是重複的,只是寫法稍變一下,重複的就不講解了,每種方法經過多線程併發進行測試,經過構造方法執行的次數查看建立對象的數量安全

【1】方法一:餓漢單例

餓漢單例,顧名思義,餓漢就是比餓,比較飢渴,比較衝動,一上來就進入主題,建立對象。markdown

  • 在類加載時就建立了單例對象
  • 只建立一個實例,絕對線程安全,在線程還沒出現之前就是實例化了,不可能存在訪問安全問題,JVM保證線程安全
  • 優勢:沒有加任何鎖,執行效率高,線程安全
  • 缺點:無論你是否用到,類裝載的時候就完成實例化,浪費內存空間(空間換時間)
public class Hungry {
    // 私有構造器
    private Hungry(){
        System.out.println(Thread.currentThread().getName());
    }
    // 指向本身實例的私有靜態引用
    private static final Hungry hungry = new Hungry();
    // 以本身實例爲返回值的靜態的公有方法
    public static Hungry getInstance(){
        return hungry;
    }
}

// 多線程併發測試
@Test
public void TestMain1(){
    for (int i = 0;i < 10;i++){
        new Thread(() -> {
            Hungry.getInstance();
        }).start();
    }
}
複製代碼

image-20210205155802668

【2】方法二:懶漢單例

懶漢模式,就是比較懶,作事比較拖拉,在調用實例方法的時候纔去實例化對象,一開始我就不給你 new 對象,你來找我,我再給你建立一個對象多線程

  • 在調用實例方法的時候纔去實例化對象
  • getInstance方法中線程不安全,會建立了多個實例
  • 優勢:一直沒人用的話,就不會建立實例,節約內存空間
  • 缺點:浪費判斷時間,下降效率(時間換空間),線程不安全
public class LazyMan {
    // 私有構造器
    private LazyMan(){
        System.out.println(Thread.currentThread().getName());
    }
    // 指向本身實例的私有靜態引用
    private static LazyMan lazyMan = null;
    // 以本身實例爲返回值的靜態的公有方法
    public static LazyMan getInstance(){
        if(lazyMan == null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

// 多線程併發測試
@Test
public void TestMain2(){
    for (int i = 0;i < 10;i++){
        new Thread(() -> {
            LazyMan.getInstance();
        }).start();
    }
}
複製代碼

image-20210205165553057

【3】方法三:懶漢單例增強(同步方法)

方法二存在線程不安全的隱患,咱們能夠經過加鎖來進行增強,方法聲明上加上 synchronized,同步方法併發

  • 和方法二同樣,在調用實例方法的時候纔去實例化對象
  • 在方法聲明上加鎖,保證線程安全
  • 優勢:一直沒人用的話,就不會建立實例,節約內存空間,且線程安全
  • 缺點:每次建立實例都要判斷鎖,浪費判斷時間,下降效率(時間換空間)
public class LazyManSyn {
    // 私有構造器
    private LazyManSyn(){}
    // 私有靜態引用建立對象
    private static LazyManSyn lazyManSyn = null;
    // 同步方法建立實例對象
    public static synchronized LazyManSyn getInstance(){
        if (lazyManSyn == null){
            lazyManSyn = new LazyManSyn();
        }
        return lazyManSyn;
    }
}

// 多線程併發測試
@Test
public void TestMain3(){
    for (int i = 0; i < 10; i++) {
        new Thread(()->{
            System.out.println(LazyManSyn.getInstance().hashCode());
        }).start();
    }
}
複製代碼

image-20210205225829847

【4】方法四:懶漢單例增強(DCL)

方法三雖然線程安全,但每次建立實例都須要判斷鎖,效率低,咱們能夠經過雙重鎖來進行增強,讓已經有實例對象的時候不進行鎖判斷,這種方式稱之爲DCL懶漢式性能

  • 和方法二同樣,在調用實例方法的時候纔去實例化對象
  • 經過雙重鎖來防止多實例(雙重判斷和鎖)
    • 第一個 if 判斷:提升執行效率,若是對象不爲空,就直接不執行下面的程序
    • 第二個 if 判斷:在第一個 if 判斷和 synchronized 鎖之間,能夠進來多個線程,若是沒有第二個 if 判斷,一個線程拿到鎖 new 對象後釋放鎖,第二個線程又可以拿到鎖 new 對象,經過第二個 if 則能夠避免建立多個對象。
    • synchronized 鎖:經過同步代碼塊
  • 優勢:減小了鎖的判斷,相對於方法三提高了效率,使用雙重鎖,暫且認爲線程安全(後面再說)
  • 缺點:其實這個方法仍是存在線程不安全的隱患,在lazyManDCL = new LazyManDCL();建立對象的時候,不是一個原子性操做,會出現指令重排的可能,所以也可能建立多個實例
public class LazyManDCL {
    // 私有構造器
    private LazyManDCL(){}
    // 指向本身實例的私有靜態引用
    private static LazyManDCL lazyManDCL = null;
    // 雙重檢查鎖模式
    public static LazyManDCL getInstance(){
        if(lazyManDCL == null){
            synchronized (LazyManDCL.class){
                if(lazyManDCL == null){
                    lazyManDCL = new LazyManDCL();
                }
            }
        }
        return lazyManDCL;
    }
}

// 多線程併發測試
@Test
public void TestMain3(){
    for (int i = 0;i < 10;i++){
        new Thread(() -> {
            System.out.println(LazyManDCL.getInstance().hashCode());
        }).start();
    }
}
複製代碼

image-20210206103944913

【5】方法五:DCL增強

在DCL單例模式中,雖然可以經過雙重鎖保證必定的線程安全性,可是在 new 對象的時候,非原子性操做形成指令重排,執行 new LazyManDCL(); 的過程以下:測試

  1. 分配內存空間
  2. 執行構造方法
  3. 把這個對象指向這個空間

執行代碼時,爲了提升性能,編譯器和處理器每每會對指令進行重排序,上面的執行順序多是 1—>2—>3,也多是 1—>3—>2,當執行順序爲 1—>3—>2 時,就會出現以下問題:spa

  • A線程執行 1—>3 ,分配了內存空間,把這個對象指向這個空間
  • 此時B線程進來,因爲A線程指向了這個空間,形成第一個 if 判斷爲 false,從而直接 return 對象,因爲沒有初始化對象,就會報錯

所以咱們要防止指令重排的現象發生,即:使用 volatile 關鍵字,代碼以下,就是在方法四的基礎上加了一個 volatile 關鍵字線程

  • 優勢:線程安全
  • 缺點:多重判斷,浪費時間,下降效率
public class LazyManDCL {
    // 私有構造器
    private LazyManDCL(){}
    // 指向本身實例的私有靜態引用,使用volatile防止指令重排
    private static volatile LazyManDCL lazyManDCL = null;
    // 雙重檢查鎖模式
    public static LazyManDCL getInstance(){
        if(lazyManDCL == null){
            synchronized (LazyManDCL.class){
                if(lazyManDCL == null){
                    lazyManDCL = new LazyManDCL();
                }
            }
        }
        return lazyManDCL;
    }
}

// 多線程併發測試
@Test
public void TestMain4(){
    for (int i = 0;i < 10;i++){
        new Thread(() -> {
            System.out.println(LazyManDCL.getInstance().hashCode());
        }).start();
    }
}
複製代碼

image-20210206115800861

【6】方法六:懶漢單例增強(靜態內部類)

經過靜態內部類來建立實例,靜態內部類單例模式的核心原理爲對於一個類,JVM在僅用一個類加載器加載它時,靜態變量的賦值在全局只會執行一次!,

  • 優勢1:外部類加載時,不會當即加載內部類,所以不佔內存
  • 優勢2:不像DCL那樣須要進行多重判斷,提高了效率
  • 優勢3:第一次調用getInstance()方法時,虛擬機才加載SingleOne類,既能保證線程安全,又能保證明例的惟一性,同時也延遲了單例的實例化
public class SingleOne {
    // 私有構造方法
    private SingleOne(){}
    // 經過靜態內部類建立實例
    private static class InnerClass{
        private static final SingleOne SINGLE_ONE = new SingleOne();
    }
    // 經過內部類返回對象實例
    public static SingleOne getInstance(){
        return InnerClass.SINGLE_ONE;
    }
}

// 多線程併發測試
@Test
public void TestMain5(){
    for (int i = 0; i < 10; i++) {
        new Thread(()->{
            System.out.println(SingleOne.getInstance().hashCode());
        }).start();
    }
}
複製代碼
image-20210206185449387

【6】方法七:枚舉單例(最完美寫法)

上面的方法都是在忽略反射的狀況下,咱們都知道,反射能夠破壞類,可以無視私有構造器,所以,上面的單例均可以使用反射進行破壞,爲了解決反射破壞,咱們可使用枚舉單例。

  • 枚舉的特性自己就是單例,在任何狀況下都是一個單例
  • 直接經過EnumSingle.INVALID進行調用
  • 讓JVM來幫咱們保證線程安全和單一實例問題
public enum EnumSingle {
    INVALID;
    public void doSomething(){}
}

// 多線程併發測試
@Test
public void TestMain6(){
    for (int i = 0; i < 10; i++) {
        new Thread(()->{
            System.out.println(EnumSingle.INVALID.hashCode());
        }).start();
    }
}
複製代碼
image-20210206204120457

總結

單例模式寫法有不少種,稍微改動一下可能又是一種,不過最完美的仍是方法七的枚舉單例,可是用的最多的仍是第一種,由於簡單,易於理解,更適合開發者。其實咱們沒有必要拘泥於完美,最合適的纔是最好的,用什麼方式解決實際問題更合適就用什麼方式,不要追求那些沒必要要的完美。就像兩我的在一塊兒,可能他(她)足夠完美,你很喜歡,然而卻由於種種緣由不是那麼合適,喜歡是乍見之歡,久處仍怦然,合適是你來我往,聽得懂和聊得來,你是用心動的方法仍是用本身可以信手拈來的方法呢?固然,即喜歡又合適是最好的,不管多難,你也總會遇到即喜歡有合適的,畢竟地球是圓的,只是時間遲早的問題!

相關文章
相關標籤/搜索