架構師的成長之路 —— 深度剖析單例模式(必定會顛覆你的認知)

單例模式是咱們最最經常使用的一種設計模式,我以爲但凡接觸過Java的朋友必定或多或少的瞭解過,而且單例模式在不少的面試中也是一個很是高頻的考點,那麼咱們該怎麼去分析理解讓咱們把這種設計模式緊緊掌握,在代碼設計或者面試中成爲本身的一個加分項。那麼這篇文章將深刻剖析單例模式,確定會讓你對單例模式有一個顛覆性認知和質的飛躍。廢話很少說Do It !!java

1.餓漢單例模式:

咱們先來看一下餓漢模式,這個應該是單例模式的一個入門,那麼咱們來聊一下餓漢模式的2種寫法,分別對比一下他們的優缺點,而後來再進行分析和優化面試

1.1 普通餓漢模式:

咱們普通餓漢模式的寫法一共就分三步:spring

  1. 私有化構造方法
  2. 使用public static final 修飾,在類加載的時候加載這個對象實例
  3. 提供一個對外訪問點

是否是特別的簡單,那麼咱們來分析一下這個代碼優缺點:
首先普通餓漢模式的優勢:它的效率很高,當類加載的時候就會初始化對象實例,他是一個線程安全的單例。
缺點就是他會浪費內存資源,就像spring同樣,咱們須要將不少的been交給spring管理,可是咱們沒有使用的時候spring不可能將這個been初始化到內存,那樣性能極其低下。

編程

public class HungrySingleton {
    //1.私有化構造方法
    private HungrySingleton(){}

    //2.使用public static final 修飾,在類加載的時候加載這個對象實例
    private static final HungrySingleton instance = new HungrySingleton();

    //3.提供一個對外訪問點
    public static HungrySingleton getInstance(){
        return instance;
    }
}

1.2靜態代碼塊的餓漢模式:

那麼咱們使用靜態代碼塊實現單例的寫法一共須要4步:設計模式

  1. 私有化構造方法
  2. 定義一個靜態常量實例,先不去進行賦值
  3. 使用一個靜態代碼塊將常量進行賦值
  4. 提供一個公共訪問點

那麼咱們分析一下這個餓漢單例,他跟咱們普通餓漢單例實際上是沒有任何區別的,都是在類加載的時候對常量進行賦值。我的以爲優勢:能夠裝一下逼,毫無卵用。
那麼它的缺點跟普通餓漢同樣,會形成內存資源的浪費,那麼這個時候咱們該怎麼解決這個問題,那麼就須要引出下一個單例模式,懶漢式單例模式。
安全

public class HungryStaticSingleton {
    //1.私有化構造方法
    private HungryStaticSingleton(){}

    //2.定義一個靜態常量實例,先不去進行賦值
    private static final HungryStaticSingleton INSTANCE;

    //3.使用一個靜態代碼塊將常量進行賦值
    static {
        INSTANCE = new HungryStaticSingleton();
    }

    //4.提供一個公共訪問點
    public static HungryStaticSingleton getInstance(){
        return INSTANCE;
    }
}

2.懶漢單例模式:

針對餓漢單例模式的缺點咱們引出了懶漢單例模式,那麼懶漢單例模式會是最完美的單例模式嗎?咱們就來分析一下框架

2.1 普通懶漢單例模式:

這個就是普通的懶漢單例模式它的實現步驟分紅四步:性能

  1. 1.私有化構造方法
  2. 定義一個靜態變量來接受對象實例,先不去賦值
  3. 提供一個公共訪問點
  4. 判斷當前這個靜態變量是否有值,若是有值咱們就直接返回這個實例,沒有值咱們就去建立一個對象並賦值

那麼咱們來分析一下普通的懶漢單例模式的優缺點,它的優勢就是解決了一個內存浪費的問題,當咱們調用getInstance的時候纔去建立一個對象實例。那麼***它的缺點也很明顯就是存在線程安全問題***。那麼咱們下面來深度分析一下他究竟爲何存在線程安全問題。學習

public class LazySimpleSingleton {
    
    //1.私有化構造方法
    private LazySimpleSingleton(){}
    
    //2.定義一個靜態變量來接受對象實例,先不去賦值
    private static LazySimpleSingleton instance;
    
    //3.提供一個公共訪問點
    public static LazySimpleSingleton getInstance(){
        //4.判斷當前這個靜態變量是否有值,若是有值咱們就直接返回這個實例,沒有值咱們就去建立一個對象並賦值
        if(instance == null){
            instance = new LazySimpleSingleton();
        }
        return instance;
    }
}

2.1.1 懶漢式單例模式的線程安全問題:

首先第一步:咱們建立一個類實現Runnable接口,重寫Run方法,在run方法裏面調用咱們的普通餓漢單例的getInstance()方法。並打印。
在這裏插入圖片描述第二步:咱們寫一個測試類,在main方法裏面開啓兩個線程。
在這裏插入圖片描述第三步:分別在咱們的懶漢單例類,測試類,線程類中打上斷點並開啓線程模式進行測試。

測試

2.1.2 進行測試

首先咱們不去管任何問題進行測試,看一下結果如何:

第一次測試結果:感受咱們的單例模式沒問題,很完美。
在這裏插入圖片描述第二次測試結果:臥槽,兩個對象徹底不同,什麼狀況。
在這裏插入圖片描述
那麼咱們來分析一下爲何會出現不同的測試結果。


咱們如今分別來分析一下究竟是如何出現相同結果,出現相同結果的時候又究竟是不是線程安全的。

出現相同的結果:

1.確實是單例模式的,兩個線程並無發生線程安全問題,都是按照順序分別執行的。
這個咱們就不去分析了,很簡單就是兩個線程沒有發生衝突,分別執行。
2.還有一種狀況就是發生了線程安全問題,線程2覆蓋了線程1的地址,得出的結果是一個僞結果

那麼咱們來好好分析一下這樣的狀況。 咱們已經在測試類,線程類和懶漢類中打上了線程模式的斷點,那麼咱們來模擬一下它是如何進行覆蓋的。

第一步咱們使用debug模式運行咱們的測試類:咱們來看一下t1和t2是否都搶到了cpu的時間片。下圖所示就是兩個線程都搶到了時間片並且他們的狀態都是Running。
在這裏插入圖片描述第二部咱們首先切換到線程1,讓線程1建立一個實例,可是不去進行打印,而後咱們來記一下這個對象實例的地址499
在這裏插入圖片描述

第三步咱們切換到線程2,然線程2也去建立一個實例,這個時候他將500覆蓋了以前的499.
在這裏插入圖片描述第四步咱們再次去切換線程1.看到咱們打印的對象地址變成了500
在這裏插入圖片描述第五步咱們來看一下這兩個線程的輸出結果是相等的,可是咱們知道這個結果實際上是僞數據,是線程2覆蓋了線程1的地址,獲得了相同的地址值,其實它的內部仍是發生了線程安全問題。
在這裏插入圖片描述


出現不一樣的結果:

1.其實就是兩個線程發生了線程安全問題,那麼咱們就來模擬一下結果不一樣的時候。

第一步仍是啓動debug模式,分別切換到線程1和線程2,讓他們都進入單例類中的判斷內

第二步切換到線程1中直接將線程1執行完成打印結果,這個時候咱們看到線程1獲得的地址是@456575b9
在這裏插入圖片描述
第三步咱們切換到線程2,將代碼執行完畢,查看結果,很明顯由於發生了線程安全問題因此獲得了兩個不一樣的對象,違背了單例模式的設計初衷
在這裏插入圖片描述


那麼如今這個問題咱們已經發現了,那麼該怎麼去解決呢?這個時候就要引入懶漢模式的第二種寫法,經過加鎖的方式來保證線程安全。

2.2 加鎖的懶漢單例模式:

很是很是簡單就是在公共訪問點上加synchronized 關鍵字來保證線程安全問題。那麼咱們來分析一下這個代碼的優缺點,優勢很明顯就是能夠保證線程安全問題了,缺點:可是咱們知道synchronized 是一個重量級的鎖,這樣會很影響性能,
那麼咱們如何對這段代碼進行優化呢?這個時候就要引入一個新的懶漢單例模式——雙重校驗鎖

public class LazyLockSingleton {

    //1.私有化構造方法
    private LazyLockSingleton(){}

    //2.定義一個靜態變量來接受對象實例,先不去賦值
    private static LazyLockSingleton instance;

    //3.提供一個公共訪問點
    public synchronized static LazyLockSingleton getInstance(){
        //4.判斷當前這個靜態變量是否有值,若是有值咱們就直接返回這個實例,沒有值咱們就去建立一個對象並賦值
        if(instance == null){
            instance = new LazyLockSingleton();
        }
        return instance;
    }
}

2.3 雙重校驗鎖懶漢單例模式:

雙重校驗鎖的寫法以下所示,這個對新手來講一點也不友好,可讀性太差

咱們來分析一下它爲何要加入兩層判斷:

第一層的if判斷是:咱們爲了提升程序的性能能夠將多個線程都先進入到getInstance中,而後經過判斷個人靜態變量是否已經被賦值。若是咱們的靜態變量沒有賦值那麼咱們就建立對象並賦值,若是這個靜態變量已經存在值了,那麼咱們就直接進行返回。

第二層if判斷的意思是:咱們先假設若是沒有這一層判斷會發生什麼狀況,線程1和線程2都進入到了這個方法中,首先線程1進行判斷靜態變量沒有進行賦值,那麼線程一就繼續進行操做,建立對象並賦值。線程2這個時候由於有Synchronized修飾,因此線程2只能進行等待,只有線程1進行建立和賦值,可是線程1對靜態變量進行賦值以後並無進行打印這個時候線程2進入到方法中,建立值,那線程2就會從新將對象覆蓋線程1建立的對象。跟咱們以前不加鎖的效果同樣, 一樣是沒有保證線程安全問題。那麼咱們再加一層if判斷,若是已經有線程對靜態變量賦值,無論它有沒有進行打印,線程2再進入方法時都會再一次進行判斷,這樣就有效的解決了線程安全問題。這個就是雙重校驗鎖的原理。

那麼咱們來總結一下雙重校驗鎖的優缺點***雙重校驗鎖的形式就能夠很好的保證了線程安全問題,也提升了程序的性能那麼它的一個缺點就是代碼變得可讀性不好,不優美。***
咱們其實一直都在尋找一個很好的解決單例的一種辦法,那麼有沒有一種很優雅,可讀性很高又線程安全的方式呢?那麼咱們來講一下下一種懶漢單例模式——靜態內部類的方式實現懶漢模式。

public class LazyDoubleCheckSingleton {

    //1.私有化構造方法
    private LazyDoubleCheckSingleton(){}

    //2.定義一個靜態變量來接受對象實例,先不去賦值
    private static LazyDoubleCheckSingleton instance;

    //3.提供一個公共訪問點
    public static LazyDoubleCheckSingleton getInstance(){
        //4.判斷當前這個靜態變量是否有值,若是有值咱們就直接返回這個實例,沒有值咱們就去建立一個對象並賦值
        if(instance == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(instance == null){
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

2.3 靜態內部類的懶漢單例模式:

咱們先來看一下代碼,這個時候確定會有人問,這個和餓漢模式同樣呀,都是對這個實例對象賦值,其實這個方式是巧妙的運用了Java語法的特色,咱們來分析一下:一個類被加載的時候是生成一個字節碼文件,根據下面例子,就會生成一個LazyInnerClassSingleton.class字節碼文件,可是這個內部類並不會立刻生成字節碼文件,當咱們調用getInstance方法的時候就會生成一個LazyInnerClassSingleton$InnerClass.class字節碼文件,這個時候就是將這個對象實例進行賦值,那麼這個靜態內部類的方式就完美的解決了延時加載,線程安全,性能高的問題。那麼咱們來思考一下,這個方法究竟是不是最佳的單例模式,它已經接近完美了,那還會出現什麼問題,其實這個方式的確能夠解決咱們以前的問題,可是人無完人代碼同樣也是,其實它還有一個問題就是能夠經過反射來破壞單例模式。包括上面說的4中方式都會被反射破壞

public class LazyInnerClassSingleton {
    //1.私有化構造方法
    private LazyInnerClassSingleton(){}

    //2.提供一個公共訪問點
    public static LazyInnerClassSingleton getInstance(){
        return InnerClass.instance;
    }
    //3.經過靜態內部類來對這個對象實例進行賦值
    private static class InnerClass{
        private static LazyInnerClassSingleton instance = new LazyInnerClassSingleton();
    }
    
}

咱們來分析如何使用反射來破壞單例模式:

提示,設計模式無時無刻不在使用反射技術,若是你們對反射有一些生疏或者忘記了,那麼能夠先去複習學習一下。這樣對設計模式或者框架的學習頗有幫助。

那麼咱們就來一個測試類,經過反射的方式看看如何來破壞單例的,就拿靜態內部類的方式來測試。

第一步咱們想要經過反射建立對象首先要或者這個類的構造方法。
第二部咱們經過設置權限來進行建立對象
而後咱們對比建立的對象的地址值是否同樣

public class InnerClassSingletonTest {
    public static void main(String[] args) {
        try {
            //經過className來獲取這個類對象
            Class clazz = Class.forName("cn.xiaomin.singleton_csdn.lazysingleton.LazyInnerClassSingleton");
            //再經過這個類對象獲取它的構造方法
            Constructor constructor = clazz.getDeclaredConstructor();
            //打印這個構造方法
            System.out.println(constructor);
            //設置最高權限來進行訪問
            constructor.setAccessible(true);
            //分別建立兩個對象,在進行對比兩個對象的地址是否同樣
            Object instance1 = constructor.newInstance();
            Object instance2 = constructor.newInstance();
            System.out.println(instance1 == instance2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

那麼如今咱們來看一下它的結果是不是同樣的,很明顯結果是false,證實咱們成功的經過反射來繞過getInstance這個方法建立對象
在這裏插入圖片描述
那麼如今問題又來了,那麼還有什麼方式來建立單例模式能夠避免經過反射的技術來破壞單例呢?那麼咱們如今來說一個註冊式單例模式。

2.3 註冊式單例模式——枚舉單例模式:

那麼咱們先來看一下用枚舉的方式如何實現單例模式:
這個就是一個枚舉類的單例。

public enum  EnumSingleton {
    INSTANCE;
    
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
    
}

咱們使用枚舉類的單例的方式:
咱們經過枚舉單例中字段的get/set方法去設置值和取值

public class EnumSingletonTest {
    public static void main(String[] args) {
        EnumSingleton instance = EnumSingleton.getInstance();
        instance.setData(new Object());
    }
}

那麼爲何枚舉類的單例不會被反射來破壞呢?咱們來經過反射來測試一下看一下經過反射來建立對象會怎麼樣?
咱們第一步經過反射來獲取這個枚舉類的構造方法,咱們先打印一下這個構造方法,看看結果怎麼樣

public class EnumSingletonTest {
    public static void main(String[] args) {
// EnumSingleton instance = EnumSingleton.getInstance();
// instance.setData(new Object());
        try {
            Class clazz = Class.forName("cn.xiaomin.singleton_csdn.enumsingleton.EnumSingleton");
            Constructor constructor = clazz.getDeclaredConstructor();
            System.out.println(constructor);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

打印結果爲:沒有找到這個構造方法,那麼咱們從源碼中找一下答案,看一下這個構造方法爲何沒有。
在這裏插入圖片描述咱們知道枚舉類實際上是繼承了Enum類,那麼咱們來看一下Enum這個類中的構造方法是什麼?很明顯這個Enum類中有一個構造方法裏面有兩個參數,一個是String一個是int,原來是這個樣子 ,那麼咱們從新來獲取一下構造方法。
在這裏插入圖片描述
再一次獲取構造方法:


public class EnumSingletonTest {
    public static void main(String[] args) {
// EnumSingleton instance = EnumSingleton.getInstance();
// instance.setData(new Object());
        try {
            Class clazz = Class.forName("cn.xiaomin.singleton_csdn.enumsingleton.EnumSingleton");
            Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);
            System.out.println(constructor);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

咱們來看一下輸出結果:
在這裏插入圖片描述
很好,咱們已經獲取了構造方法,那麼咱們經過這個構造方法來建立對象會怎麼樣?

public class EnumSingletonTest {
    public static void main(String[] args) {
// EnumSingleton instance = EnumSingleton.getInstance();
// instance.setData(new Object());
        try {
            Class clazz = Class.forName("cn.xiaomin.singleton_csdn.enumsingleton.EnumSingleton");
            Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);
            constructor.setAccessible(true);
            Object instance = constructor.newInstance();
            System.out.println(instance);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

咱們來看一下結果:
在這裏插入圖片描述這個錯優勢意思,它居然這麼直白的去說不能經過反射來獲取枚舉類的對象。這個就頗有意思了,因此說枚舉類的單例不會被反射去破壞。

那麼咱們來分析一下枚舉類單單例模式的優缺點都有什麼?
優勢:代碼優雅,可讀性高,線程安全,完美的解決了使用反射來破壞單例的問題。
缺點:不能建立大量的實例,消耗內存資源高

由於咱們須要借鑑高人的思路,看一下大神是如何實現單例的,那麼咱們的思路就是經過散熱spring來對比它是如何實現單例的。其實spring是借鑑了枚舉的優勢,而後再進行改良實現的另外一種單例模式。那麼咱們就要引出——容器式單例模式

2.4 註冊式單例模式——容器式單例模式:

由於spring中確定有得到單例的方式,它就是借鑑了枚舉類的優勢進行改良的獲得的。那麼spring的單例模式其實就是一個容器式單例模式,經過惟一key來獲取對象。
那麼容器式單例的實現方式又是怎麼樣的呢?
這個就是容器式單例的寫法,很簡單,咱們須要私有化構造方法,而後初始化一個容器。而後提供一個對外訪問點,在getInstance中進行判斷,若是咱們的map中包含className,那麼咱們就直接將對象實例返回,若是沒有沒,那麼咱們就去建立一個對象,並把這個className惟一key存入容器中。

public class ContainerSingleton {
    private ContainerSingleton(){}
    //建立一個容器
    private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
    public static Object getInstance(String className){
        if(!ioc.containsKey(className)){
            ContainerSingleton instance = new ContainerSingleton();
            ioc.put(className,instance);
            return instance;
        }else {
            return ioc.get(className);
        }
    }

}

那麼這種寫法還會有什麼問題呢?就是他的確性能很高,spring也是用的容器式單例模式,那麼咱們這個版本還有一個缺陷,就是線程不安全,並且會存在序列化問題。那麼咱們來看一下,如何將容器式單例改進成線程安全的呢?
那麼我本身採起的方式就是經過雙重校驗鎖的方式來保證線程安全問題代碼以下:
我來解釋一下這段代碼的意思,首先咱們以前學過一個雙重校驗鎖的方式來知足線程安全問題。並且設計模式這個東西沒有惟一正確答案,它是一種設計思想,由於在spring源碼中,getBeen的時候其實他就是經過雙重校驗鎖的方式來保證線程安全問題的。

public class ContainerSingleton {
    private ContainerSingleton(){}
    //建立一個容器
    private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
    public static Object getInstance(String className){
        Object instance = null;
        if(!ioc.containsKey(className)){
            synchronized (ContainerSingleton.class){
                if(instance == null){
                    try {
                        instance = Class.forName(className).newInstance();
                        ioc.put(className,instance);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
                return instance;
            }
        }else {
            return ioc.get(className);
        }
    }

}

這樣咱們的線程安全問題已經解決了,那麼咱們如何解決序列化的問題,很是簡單:添加readResolve(),返回Object對象

3.那麼咱們來總結一下單例模式:

  1. 延時加載
  2. 線程安全
  3. 私有化構造方法
  4. 防止反射,序列化和反序列破壞

單例模式的優勢:

  1. 在內存中只有一個實例減小內存開銷
  2. 避免對資源的多重佔用
  3. 設置全局訪問點,嚴格控制訪問

單例模式的缺點:

  1. 擴展性差
  2. 沒有面向接口編程
  3. 違背了開閉原則

到這裏整個單例的介紹分析已經完成了,相信你們若是真的細心看完必定會有所收穫,並且咱們在使用單例的時候要根據本身的業務來合理的使用單例模式,很重要的一句話——約定大於配置!!!! 好了很是感謝你們能將文章看到最後,咱們一塊兒努力,作一個技術的思考者!

相關文章
相關標籤/搜索