【23種設計模式專題】一 單例模式,誰說程序猿沒有女(男)朋友

程序猿學社的GitHub,歡迎Star
github技術專題
本文已記錄到github

前言

你問程序猿小哥哥,有對象嗎
他會毫不猶豫的告訴你,,簡單呀,new一個唄。
我們知道"對象"只會有一個,畢竟,不是誰都是韋小寶,有7個老婆,可以new 7次。
那怎麼保證只會new出一個對象,這就是本文的主角單例模式
現在這年代太難了,找對象難,在程序中,new一個對象,還這麼講究。

概念

採取一定的手段,保證一個類中只有一個實例,並且只提供一個取到該實例的入口。

  • 是不是理解起來一臉懵逼,別急,後面會一一的解釋這段的意思。

應用場景

  • 學過spring的社友,應該知道,在spring中創建一個bean,會有一個scope屬性,他的值默認爲singleton,也就是單例,看看spring都在用單例,你還敢說沒有用嗎?
  • 項目的配置信息,可以考慮用單例
  • 網站的計數。統計訪問量

總結:
如果該對象頻繁的被使用,我們就可以考慮把他設計單例。

實戰篇

new兩個對象,判斷是否相等

package com.cxyxs.designmode.util;

/**
 * Description:
 * Author: 程序猿學社
 * Date:  2020/3/15 12:30
 * Modified By:
 */
public class Demo1 {
    public static void main(String[] args) {
        Girlfriend gf = new Girlfriend();
        Girlfriend gf1 = new Girlfriend();
        System.out.println(gf  == gf1);
    }
}

class  Girlfriend{

}

各位社友,覺得輸出的結果是什麼?true還是false

  • 引用類型比較的是兩個對象的地址是否相等,實例化兩次,當然不相等。
  • 各位社友想一想,每次都實例化一個對象,開銷是不是很大。作爲一個別人眼裏勤儉節約的程序猿,每個月幾乎無什麼開銷。理性的程序猿,不僅僅在生活中得保持這種良好的品質,在開發過程中,我們也應該保持好這種傳統。

餓漢式(第一種)

package com.cxyxs.designmode.util;

/**
 * Description:
 * Author: 程序猿學社
 * Date:  2020/3/15 13:15
 * Modified By:
 */
public class Demo2 {
    public static void main(String[] args) {
        Girlfriend2 gf1 = Girlfriend2.getInstance();
        Girlfriend2 gf2 = Girlfriend2.getInstance();
        System.out.println(gf1 == gf2);
    }
}
class  Girlfriend2{
    //第二步
    private  static Girlfriend2 gf = new Girlfriend2();
    // 第一步
    private Girlfriend2() {

    }

    //第三步
    public  static Girlfriend2 getInstance(){
        return  gf;
    }
}


輸出的值爲true,說明只實例化一次。我們來梳理一下代碼實現思路。

  • 爲了防止,一直new,直接把構造方法私有化,防止別人調用。
  • 通過靜態變量實現只初始化一次,被static修飾的變量,我們都知道只會加載一次,所以,就可以保證對象只會實例化一次。
  • 我們現在也無法通過new 對象,實際上就是通過調用構造方法的方式直接創建對象,那其他的人,要如何調用,是不是需要提供一個入口,因爲無法new對象,這就意味着我們無法通過對象.屬性的方式調用該方法,是不是隻有把這個方法修飾爲static方法,才能調用。

在學習單例過程中,我們會經常看到一個詞彙“懶加載”,他是什麼意思?

  • 實際上,就是需要使用的時候,才加載。案例:女朋友說喜歡草莓,你去買了一大盒,你到家後,女朋友,跟你說,我又不喜歡草莓,錢也花了,沒人喫,是不是造成浪費,在軟件的世界裏,也是一樣,避免資源的浪費。

總結:
餓漢式是通過static變量,也就是類加載原理保證單例,也就是所謂的線程安全。沒有實現懶加載。
個人的建議:
可以使用,內存浪費的問題,幾乎可以忽略,在實際開發過程中,存在一個類,也不調用,還傻傻的放在那裏,說明這個代碼,可能是無用的代碼,對於,沒有的代碼,直接幹掉。

懶漢式(第二種)

package com.cxyxs.designmode.util;

/**
 * Description:懶漢式
 * Author: 程序猿學社
 * Date:  2020/3/15 14:00
 * Modified By:
 */
public class Demo3 {
    public static void main(String[] args) {
        Girlfriend3 gf = Girlfriend3.getInstance();
        Girlfriend3 gf1 = Girlfriend3.getInstance();
        System.out.println(gf == gf1);
    }
}
class  Girlfriend3{
    private static  Girlfriend3 gf =  null;
    private Girlfriend3() {

    }
    public static Girlfriend3 getInstance(){
        if(gf == null){
            gf = new Girlfriend3();
        }
        return gf;
    };
}


打印的結果也爲true,各位社友,是不是覺得,這就實現單例?
實際上,這種寫法是有問題的,會存在線程安全的問題

  • gf對象被static修飾,說明資源只有一份,看過社長多線程高併發編程文章的社友,應該都知道,多個線程訪問同一個資源的時候,會存在線程不安全的問題。
  • 假設有兩個線程A B, A走到判斷這裏,而這是的B也剛好走到這裏,都沒有創建對象,都會創建對象。實際上就會創建實例化兩次。

模擬線程不安全

package com.cxyxs.designmode.util;

import java.util.concurrent.TimeUnit;

/**
 * Description:懶漢式,模擬線程不安全
 * Author: 程序猿學社
 * Date:  2020/3/15 14:00
 * Modified By:
 */
public class Demo4 {
    public static void main(String[] args) {
        new Thread(()->{
            Girlfriend4 gf = Girlfriend4.getInstance();
            System.out.println(gf);
        }).start();

        new Thread(()->{
            Girlfriend4 gf1 = Girlfriend4.getInstance();
            System.out.println(gf1);
        }).start();
    }
}

class  Girlfriend4{
    private static  Girlfriend4 gf =  null;
    private Girlfriend4() {

    }
    public static   Girlfriend4 getInstance(){
        if(gf == null){
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            gf = new Girlfriend4();
        }
        return gf;
    };
}

  • TimeUnit.MILLISECONDS.sleep(100); 模擬延遲100毫秒,如果不加這個代碼,程序一閃而過,是無法看到實際的效果的
  • 通過測試,我們可以發現,這兩個對象的地址是不一樣的,說明實例化兩次,所以我們說他是線程不安全的。

對多線程不瞭解社友,看到線程不安全這個詞彙,可能會有點懵逼,這裏我簡單的闡述一下,如何界定一個線程是否安全。

  • 首先,有一個前提,在多線程環境下,我上面的代碼new了兩次Thread。就是保證在多線程環境下。
  • 我們對程序有一個預期的效果,按照我們程序的預期,這個對象是實例化一次,這個就是我們的預期,最終的結果是實例化兩次,跟我們預期有偏差,我們就可以說他是線程不安全的。

模擬線程不安全,方法加synchronized

跟上面的方法相比,只是增加了synchronized關鍵字

package com.cxyxs.designmode.util;

import java.util.concurrent.TimeUnit;

/**
 * Description:懶漢式,方法上增加同步
 * Author: 程序猿學社
 * Date:  2020/3/15 14:00
 * Modified By:
 */
public class Demo5 {
    public static void main(String[] args) {
        new Thread(()->{
            Girlfriend5 gf = Girlfriend5.getInstance();
            System.out.println(gf);
        }).start();

        new Thread(()->{
            Girlfriend5 gf1 = Girlfriend5.getInstance();
            System.out.println(gf1);
        }).start();
    }
}

class  Girlfriend5{
    private static  Girlfriend5 gf =  null;
    private Girlfriend5() {

    }
    public static synchronized   Girlfriend5 getInstance(){
        if(gf == null){
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            gf = new Girlfriend5();
        }
        return gf;
    };
}

  • 爲了解決線程安全的問題,在方法上增加關鍵字synchronized,意味着,同一時候,只能有一個線程調用該方法,方法運行完後,下一個線程才能拿到鎖。 案例:每一個茅坑都有一把鎖,本來一個洗手間,可以容納很多人,使用synchronized,就相當於,隔壁小王在上洗手間,爲了防止被人偷窺,直接把洗手間的大門鎖上,需要等隔壁老王,上完後,其他人才能進去,所以說,加synchronized這種方式性能不怎麼好。

總結:
通過synchronized關鍵字,解決懶漢式單例線程安全的問題,雖說能保證線程安全,但是,效率太低,在實際項目實戰過程中,不建議使用這種方式

隔壁老王:社長,那有什麼辦法,可以保證線程安全,效率還行的實現方式嗎?
社長:有的,通過雙重校驗鎖方式。

雙重校驗鎖DCL(第三種)

1.0版本,線程不安全

package com.cxyxs.designmode.util;

import java.util.concurrent.TimeUnit;

/**
 * Description:懶漢式 雙重校驗鎖  DCL
 * Author: 程序猿學社
 * Date:  2020/3/15 14:00
 * Modified By:
 */
public class Demo6 {
    public static void main(String[] args) {
        new Thread(()->{
            Girlfriend6 gf = Girlfriend6.getInstance();
            System.out.println(gf);
        }).start();

        new Thread(()->{
            Girlfriend6 gf1 = Girlfriend6.getInstance();
            System.out.println(gf1);
        }).start();
    }
}

class  Girlfriend6{
    private static   Girlfriend6 gf =  null;
    private Girlfriend6() {

    }
    public static   Girlfriend6 getInstance(){
        if(gf == null){    //步驟1
            synchronized (Girlfriend6.class){
                if(gf == null){
                    gf = new Girlfriend6();  //步驟2
                }
            }
        }
        return gf;
    };
}

  • 通過結果查看,好像木有問題。不要被結果所矇蔽了,實際上,這種方式,還是會存在線程不安全的問題。
  • 爲了提供性能,java編譯器,會進行指令重排
    (1).分配內存空間
    (2)初始化對象
    (3) 把gf變量指向剛剛分佈的內存地址

這樣就會存在問題,假設,他的順序是1-3-2,線程A剛剛跑到1-3這一步,把gf指向一個地址的時候(表示gf這個對象不爲空,因爲已經有內存地址),而這時的線程B,剛剛到步驟1,進行對應的判斷,發現gf不爲空,直接退出程序。

題外話,淺談指令重排
 package com.cxyxs.designmode.util;

/**
 * Description:
 * Author: wude
 * Date:  2020/3/18 11:29
 * Modified By:
 */
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
    }
}


idea版本中,在類裏面右鍵,保證該類已運行,不然,會提示找不到主類的錯。

  • 實際上,我們在代碼裏面只是創建了一個main方法,實例化一個對象,而底層是這樣處理的。具體都是什麼意思。可以查查Java字節碼。

隔壁老王:社長,既然多線程環境下,會存在指令重排的問題,是不是說通過雙重校驗鎖這種方式實現單例,也不可行。
社長:既然存在指令重排的問題,jdk大佬當然也考慮這個問題,增加volatile關鍵字

2.0版本,線程安全


總結:
通過雙重檢驗鎖,很好的解決解決線程安全的問題。建議使用。實現了懶加載。

靜態內部類(第四種)

package com.cxyxs.designmode.util;

/**
 * Description:
 * Author: 程序猿學社
 * Date:  2020/3/18 19:59
 * Modified By:
 */
public class Demo7 {
    public static void main(String[] args) {
        Girlfriend7 gf = Girlfriend7.getInstance();
        Girlfriend7 gf1 = Girlfriend7.getInstance();
        System.out.println(gf == gf1);
    }
}
class  Girlfriend7{
    private Girlfriend7() {
    }

    public static  Girlfriend7 getInstance(){
        return  Instance.gf;
    }

    private  static  class Instance{
        private static final Girlfriend7 gf = new Girlfriend7();
    }
}

總結:
靜態內部類通過classloader 機制來保證單例和線程安全。同時也是懶加載的,只有使用到該靜態內部類被調用時,纔會被加載。

隔壁老王:社長,社長,聽說反射可以破壞單例,具體是怎麼一回事?
社長:既然,你提了這個問題,我們就來具體看看,爲什麼反射可以破壞單例。

反射破壞單例

實際上,不止反射可以破壞單例,通過序列化的方式也可以破壞單例。

package com.cxyxs.designmode.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;

/**
 * Description:模擬通過反射破壞單例
 * Author: 程序猿學社
 * Date:  2020/3/19 9:15
 * Modified By:
 */
public class Demo8 {
    public static void main(String[] args) throws Exception {
        Constructor<Girlfriend8> declaredConstructor = Girlfriend8.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);//暴力訪問

        Girlfriend8 gf = declaredConstructor.newInstance();
        Girlfriend8 gf1 = declaredConstructor.newInstance();
        System.out.println(gf);
        System.out.println(gf1);
    }
}

class  Girlfriend8{
    private Girlfriend8() {
    }

    public static  Girlfriend8 getInstance(){
        return  Instance.gf;
    }

    private  static  class Instance{
        private static final Girlfriend8 gf = new Girlfriend8();
    }
}

  • 通過測試結果,我們可以發現,通過反射,這個對象竟然被實例化多次。
  • 實際上,就是因爲反射可以拿到private的構造方法。

隔壁老王:社長,那怎麼解決反射可以暴力破解單例的問題?
社長:在構造方法裏面,再判斷一下這個對象是否爲空。

防止反射,增加監控代碼

package com.cxyxs.designmode.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;

/**
 * Description:模擬通過反射破壞單例
 * Author: 程序猿學社
 * Date:  2020/3/19 19:15
 * Modified By:
 */
public class Demo8 {
    public static void main(String[] args) throws Exception {
        Constructor<Girlfriend8> declaredConstructor = Girlfriend8.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);//暴力訪問

        Girlfriend8 gf = declaredConstructor.newInstance();
        Girlfriend8 gf1 = declaredConstructor.newInstance();
        System.out.println(gf);
        System.out.println(gf1);
    }
}

class  Girlfriend8{
    private Girlfriend8() {
        synchronized (Girlfriend8.class){
            if(Instance.gf == null){
                try {
                    throw new Exception("該對象已實例化,不要試圖破解!");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }

    public static  Girlfriend8 getInstance(){
        return  Instance.gf;
    }

    private  static  class Instance {
        private static final Girlfriend8 gf = new Girlfriend8();
    }
}
  • 實際上,這種方式,只是在第二次實例化的時候,增加一個判斷,發現重複實例化,就拋出一個異常,沒有從根本上解決問題。老王,你可以使用另外一種,代碼也十分的簡潔。那就是通過枚舉的方式實現單例。

枚舉(第五種)

package com.cxyxs.designmode.util;

import java.lang.reflect.Constructor;

/**
 * Description:
 * Author: 程序猿學社
 * Date:  2020/3/20 19:55
 * Modified By:
 */
public enum  Demo9 {
    GF;
    public Demo9 getInstance(){
        return GF;
    }
}
class  Test9{
    public static void main(String[] args) throws Exception{
        Demo9 instance = Demo9.GF.getInstance();
        Demo9 instance1 = Demo9.GF.getInstance();
        System.out.println(instance == instance1);
    }
}

  • 通過枚舉實現單例,十分的簡單,並且是線程安全的,不會被反射,序列化破壞,但是,他不支持懶加載。

隔壁老王:社長,爲什麼枚舉可以解決別人暴力破解的問題?
社長: 給你看一段源碼,你就知道爲什麼枚舉可以防止別人暴力破解。

還記得我在上一個事例中,給你說過,通過反射可以單例,寫了一段代碼,我們來看看源碼。

  • 如果類型爲枚舉,是不會繼續向下執行的,也就不會重新實例化一個對象。


最近有不少讀者在問我java應該如何學習,在這裏,把我整理的學習視頻分享出來。
(1).springboot,springcloud視頻
(2).架構師視頻,設計模式視頻,深入jvm內核原理。
(3) java面試視頻

可以通過公衆號“程序猿學社”,回覆關鍵字"視頻",希望能幫到你。

原創不易,不要白嫖,覺得有用的社友,給我點贊,讓更多的老鐵看到這篇文章。

作者:程序猿學社
原創公衆號:『程序猿學社』,暫時專注於java技術棧,分享java各個技術系列專題,以及各個技術點的面試題。
原創不易,轉載請註明來源(註明:來源於公衆號:程序猿學社, 作者:程序猿學社)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章