單例模式大全,反射拆解!你要的8種單例都在這!

 

單例模式幾乎是面試中必考的設計模式。單例到底有幾種寫法呢?每種寫法有什麼不同,怎麼來保證線程安全?反射怎麼破解單例模式?被破解後又該怎麼更改?本文用奇淫技巧的方式,帶你玩轉單例模式。單例模式大全,反射爆破拆解!你要的8種單例都在這!

本文講述單例設計模式的8種方式,反射和單例的相愛相殺🙃


單例模式

單例模式:類的對象有且只有一個

首先控制對象的產生數量:將構造方法私有化(從源頭控制對象數量,控制構造方法)

構造方法私有化:

  • 任何其他類均無法參生此對象(本質是任何他類均無法調用構造方法,所以無法產生對象)

  • 唯一的一個對象產生於類內部

  • 唯一的屬性爲<靜態屬性>,並且類中提供靜態方法取得此對象。因爲類的外部無法產生對象,因此無法調用對象方法

1. 餓漢式–靜態常量

餓漢式單例,顧名思義,就是很飢渴🙃,一上來就new產生實例化對象

/**
 * 餓漢式三個核心組成
 *   1.構造方法私有化
 *   2.類內部提供靜態私有域
 *   3.類內部提供靜態方法返回唯一對象
 */

class Singletons {
    //唯一的對象在類加載時產生
    private final static Singletons single = new Singletons();

    //構造方法私有化

    private Singletons() { }

    //靜態方法-----爲什麼是靜態方法??
    //因爲在類的外部無法產生對象,因此無法調用對象方法
    //通過getter方法取得唯一的對象
    public static Singletons getInstance(){
        return single;
    }

    public void print() {
        System.out.println("餓漢式單例,上來直接new……");
    }
}

public class HungrySingleton01 {
    public static void main(String[] args) {
        //不能直接new,而是通過 Singleton.getInstance()靜態方法取得類中已經產生好的對象
        Singletons single = Singletons.getInstance();
        Singletons single1 = Singletons.getInstance();
        System.out.println(single == single1);
        single.print();
    }
}

因爲是靜態常量,single和single1一定是同一個對象,所在的內存地址是相同的

餓漢式單例 (靜態常量)

【優點】:書寫簡單,類加載時就完成了實例化,避免了線程同步問題

【缺點】:在類加載就完成實力化,沒有達到懶加載的效果。如果從始至終沒有使用過這個實例對象,會造成內存浪費

【總結】

  • 可用,但是可能會造成內存資源的浪費

2. 餓漢式–靜態代碼塊

class Singleton02 {
    private static Singleton02 single;

    private Singleton02() { }

    static {
        single = new Singleton02();
    }

    public static Singleton02 getInstance(){
        return single;
    }

    public void print() {
        System.out.println("餓漢式單例,靜態代碼塊方式");
    }
}

public class HungrySingleton02 {
    public static void main(String[] args) {
        Singleton02 single = Singleton02.getInstance();
        Singleton02 single1 = Singleton02.getInstance();
        System.out.println(single == single1);
        single.print();
    }
}
複製代碼

餓漢式單例 (靜態代碼塊)

這種方式的優缺點和上面第一種靜態變量的沒差別,區別就是初始化的位置不同,初始化的過程放到了靜態代碼塊。

3. 懶漢式–線程不安全

當第一次去使用Singleton對象的時候纔會爲其產生實例化對象

通過一個靜態公有方法,當使用到該方法時,才創建對象(懶漢式)

/**
 * @Author: Mr.Q
 * @Description:懶漢式單例---線程不安全
 * 特點: 當第一次去使用Singleton對象的時候纔會爲其產生實例化對象的操作.
 */
class Singleton {

    private static Singleton single;

    //private 聲明無參構造
    private Singleton() { }

    //靜態公有方法,當使用到該方法時,才創建對象(懶漢式)
    public static Singleton getInstance(){
        if(single == null) {
            single = new Singleton();
        }
        return single;
    }

    public void print() {
        System.out.println("懶漢式單例(線程不安全),用的時候再new產生對象……");
    }
}

public class LazySingleton {
    public static void main(String[] args) {
        Singleton single = Singleton.getInstance();
        Singleton single1 = Singleton.getInstance();
        System.out.println(single == single1);
        single.print();
    }
}
複製代碼

懶漢式單例 (線程不安全)

【優缺點】

這種寫法是存在線程安全問題的。類比於上面兩種餓漢式單例模式,它們在沒有調用時雖然會造成內存資源的浪費,但是是安全的。因爲在類加載時就完成了實例化,避免了線程同步問題。

但是這種懶漢式寫法,起到了懶加載效果,但是隻能在單線程下使用

【線程安全問題分析】

在多線程場景下,一個線程進入了getInstance方法的if條件判斷if(single == null),還沒來得及繼續向下執行,另一個新進入的線程也通過了這個判斷語句,這是就會產生多個實例,就不是單例的了。

所以此方法在多線程場景下不可使用。

4. 懶漢式–同步方法

既然線程不安全,那我們給他加把鎖在getInstance方法上保證線程安全。

/**
 * @Author: Mr.Q
 * @Description:懶漢式單例---同步方法(效率太低)
 */
class Singleton04 {

    private static Singleton04 single;

    //private 聲明無參構造
    private Singleton04() { }

    //靜態公有方法,當使用到該方法時,才創建對象(懶漢式)
    public synchronized static Singleton04 getInstance(){
        if(single == null) {
            single = new Singleton04();
        }
        return single;
    }

    public void print() {
        System.out.println("懶漢式單例(線程安全),同步方法效率太低");
    }
}

public class LazySingleton04 {
    public static void main(String[] args) {
        Singleton04 single = Singleton04.getInstance();
        single.print();
    }
}
複製代碼

懶漢式單例(同步方法)

【優點】:解決了線程不安全的問題

【缺點】

  • 效率太低。每個線程想要獲取類的實例時,都要等在getInstance這個同步方法外,串型執行。但是由於是單例模式,只會產生一個實例化對象,第一個線程實例化完對象之後,後面的線程便不需要執行if的條件判斷了,直接return即可,但是在進入同步方法時每次都要等待,效率太低。

5. 懶漢式–同步代碼塊

先來說一種錯誤示範

if條件中添加同步代碼塊

 

 

這段代碼看起來很完美,很可惜,它是有問題。主要在於single= new Singleton()這句,這並非是一個原子操作。

此處由於不是原子操作,編譯器可能會產生指令重排的問題,所以需要保證原子性,同時加上雙重if條件判斷。懶漢式 (同步代碼塊)正確的寫法應該是雙重檢查DCL

6. 雙重檢查DCL

volatile關鍵字修飾,輕量級鎖,可以使修改值修改後立即更新到主存

private volatile static SafeSingleton single = null;
複製代碼

【這裏添加volatile的原因是】

single = new SafeSingleton();
複製代碼

創建對象這條語句不是原子操作

new關鍵字創建對象的過程分爲三步:

  1. 分配內存空間;

  2. 堆內存上創建對象(執行構造方法);

  3. 將對象的引用指向堆內存;

由於不是原子操作,就可能產生指令重排的問題。

步驟2和步驟3可能會被編譯器指令重排,1 -> 2 -> 3的執行順序變爲了1 -> 3 -> 2

先把對象的應用空間指向堆空間,然後再在堆上創建對象。(理解爲圖書館佔座位,人還沒到,但是位置上卻被佔用了)

 

 

判斷非空,但是實際拿到的對象還未完成初始化去創建,就會出現空指針異常

所以要防止指令重排,保證有序性,及時通知其線程single的實時狀態,就必須加上volatile關鍵字來防止指令重排,保證1 -> 2 -> 3的執行順序。

class SafeSingleton {

    //使用volatile關鍵字保其可見性
    private volatile static SafeSingleton single = null;

    private SafeSingleton() { }

    //同步代碼塊上鎖
    public static SafeSingleton getInstance() {
        if(single == null) {
            synchronized (SafeSingleton.class) {
                //雙重檢查
                if (single == null) {
                    single = new SafeSingleton();
                }
            }
        }
        return single;
    }

    public void print() {
        System.out.println("雙重檢測鎖的DCL單例");
    }
}

public class ReflectDCL {
    public static void main(String[] args) {
        //靜態方法取得類中已經產生好的對象
        SafeSingleton single = SafeSingleton.getInstance();
        single.print();
    }
}
複製代碼

【雙重檢查分析】

  • Double-Check概念是多線程開發中常使用到的,如代碼中所示,我們進行了兩次if(single == null)的檢查,這樣就可以保證線程安全了。

  • 這樣,實例化代碼只用執行一次,後面再次訪問時,判斷if(single == null)直接 return實例化對象,也避免的反覆進行方法同步

  • 線程安全;延遲加載;效率較高

7. 靜態內部類

我們首先對靜態內部類做一個回顧👉還好面試官還沒問,趕緊把【內部類】的知識點補上

靜態內部類也是作爲一個外部類的靜態成員而存在,創建一個類的靜態內部類對象不需要依賴其外部類對象

  • 在外部類加載時,靜態內部類不會被立即加載,而是在外部類中被使用時纔會加載,這符合懶加載的策略。

  • 當我們在外部類中調用靜態內部類時,會被加載,並且只會被加載一次,在加載時線程是安全的,保障了線程的安全性。

class StaticInner {

    private StaticInner() { }

    //靜態內部類
    private static class Singleton {
        private static final StaticInner INSTANCE = new StaticInner();
    }

    public static StaticInner getSingleton() {
        return Singleton.INSTANCE;
    }

    public void print() {
        System.out.println("靜態內部類的線程安全的懶漢式單例");
    }
}

public class StaticInnerSingle06 {
    public static void main(String[] args) {
        StaticInner single = StaticInner.getSingleton();
        single.print();
    }
}
複製代碼

靜態內部類

  1. 這種方式採用了類裝載的機制來保證初始化實例時只有一個線程
  2. 靜態內部類方式在外部類被加載時並不會立即變例化,而是在需要實例化時,調用getSingleton方法,纔會裝載 Singleton內部類,從而完成外部類的實例化。
  3. 類的靜態屬性只會在第一次加載類的時候初始化,所以在這裏,JVM幫助我們保證了線程的安全性,在類進行初始化時,別的線程是無法進入的
  4. 優點:避免了線程不安全,利用靜態內部類的特點實現延遲加載,效率高

8. 枚舉

這藉助DK15中添加的枚舉來實現單例模式。不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。

enum Singleton {
    INSTANCE; //屬性

    public static Singleton getInstance() {
        return Singleton.INSTANCE;
    }
}

public class Enum07 {
    public static void main(String[] args) {
        Singleton single = Singleton.getInstance();
        Singleton single1 = Singleton.getInstance();
        System.out.println(single == single1);
    }
}
複製代碼

9. 反射!爲所欲爲?

先來對反射的內容做個回顧👉反射,就是要爲所欲爲

DCL雙重檢查破壞

通過反射或者序列化會破壞單例,我們就以線程安全的DCL單例來說明。

還是tittle6的代碼,我們通過反射來破壞

public static void main(String[] args) throws Exception {
    SafeSingleton single = SafeSingleton.getInstance();
    Constructor<SafeSingleton> dc = SafeSingleton.class.getDeclaredConstructor();
    dc.setAccessible(true);
    SafeSingleton singleCopy = dc.newInstance();
    //false,單例被破壞
    System.out.println(singleCopy == single);
}
複製代碼

結果輸出:false

輸出爲false,說明單例模式創建了兩個對象,被反射破壞了。那如何解決呢?

首先,反射是通過無參構造來創建class對象的,我們在SafeSingleton的構造中再加一把鎖來判斷:

class SafeSingleton {

    //使用volatile關鍵字保其可見性
    private volatile static SafeSingleton single = null;

    private SafeSingleton() {
        synchronized (SafeSingleton.class) {
            if (single != null) {
                throw new RuntimeException("Don't destroy by reflection");
            }
        }
    }

    //同步代碼塊上鎖
    public static SafeSingleton getInstance() {
        if(single == null) {
            synchronized (SafeSingleton.class) {
                //雙重檢查
                if (single == null) {
                    single = new SafeSingleton();
                }
            }
        }
        return single;
    }
}

public class ReflectDCL {
    public static void main(String[] args) throws Exception {
        SafeSingleton single = SafeSingleton.getInstance();
        Constructor<SafeSingleton> dc = SafeSingleton.class.getDeclaredConstructor();
        dc.setAccessible(true);
        SafeSingleton singleCopy = dc.newInstance();
        //false,單例被破壞
        System.out.println(singleCopy == single);
    }
}
複製代碼

 

 

問題解決,此時反射無法創建對象。

問題又雙出現

剛纔單例的對象是通過私有構造方法創建的,即調用了getInstance()方法。但是,我不用這樣創建,我唯一一個對象也是通過反射來創建呢?

SafeSingleton single = SafeSingleton.getInstance();
複製代碼

換成

SafeSingleton single = dc.newInstance();
複製代碼

 

 

這時,單例模式又出幺蛾子了,又被反射爆破了!

 

 

問題解決

我們可以通過添加一個標誌位flag來判斷,防止反射破壞

class SafeSingleton03 {

    //使用volatile關鍵字保其可見性
    private volatile static SafeSingleton03 single = null;
    //添加標誌位
    private static boolean flag = false;

    private SafeSingleton03() {
        synchronized (SafeSingleton03.class) {
            if (flag == false) {
                flag = true;
            }else {
                throw new RuntimeException("Don't destroy by reflection");
            }
        }
    }

    //同步代碼塊上鎖
    public static SafeSingleton03 getInstance() {
        if(single == null) {
            synchronized (SafeSingleton03.class) {
                //雙重檢查
                if (single == null) {
                    single = new SafeSingleton03();
                }
            }
        }
        return single;
    }

    public static void main(String[] args) throws Exception {
        Constructor<SafeSingleton03> dc = SafeSingleton03.class.getDeclaredConstructor();
        dc.setAccessible(true);
        SafeSingleton03 single = dc.newInstance();
        SafeSingleton03 singleCopy = dc.newInstance();

        System.out.println(single);
        System.out.println(singleCopy);
        System.out.println(single == singleCopy);
    }
}

複製代碼

再次執行,我們發現標誌位法可以攔截兩次反射的破壞。

 

 

問題又雙叒出現

在反射中,我們不僅可以獲取構造方法呀,還可以獲取成員變量呀。那flag通過反射獲取並修改,不就有不行了?

class SafeSingleton03 {

    //使用volatile關鍵字保其可見性
    private volatile static SafeSingleton03 single = null;
    //添加標誌位
    private static boolean flag = false;

    private SafeSingleton03() {
        synchronized (SafeSingleton03.class) {
            if (flag == false) {
                flag = true;
            }else {
                throw new RuntimeException("Don't destroy by reflection");
            }
        }
    }

    //同步代碼塊上鎖
    public static SafeSingleton03 getInstance() {
        if(single == null) {
            synchronized (SafeSingleton03.class) {
                //雙重檢查
                if (single == null) {
                    single = new SafeSingleton03();
                }
            }
        }
        return single;
    }

    public static void main(String[] args) throws Exception {
        Constructor<SafeSingleton03> dc = SafeSingleton03.class.getDeclaredConstructor();
        dc.setAccessible(true);
        SafeSingleton03 single = dc.newInstance();

        //再次通過反射修改屬性值
        Field flag = SafeSingleton03.class.getDeclaredField("flag");
        flag.setAccessible(true);
        flag.set(dc,false);

        SafeSingleton03 singleCopy = dc.newInstance();

        System.out.println(single);
        System.out.println(singleCopy);
        System.out.println(single == singleCopy);
    }
}
複製代碼

通過代碼驗證,我們發現確實又雙出現問題了!

 

 

那這,又該怎麼搞?

問題,就出在了newInstance方法上,通過反射來創建對象。

我們點開源碼看看👀

 

 

咦,枚舉自帶單例模式,反射還破壞不了。是這樣嗎?我們繼續驗證

問題最終解決

測試反射能否破壞枚舉式單例

enum EnumSingleton {
    INSTANCE;
}

public class EnumTest {
    public static void main(String[] args) throws Exception {
        EnumSingleton single = EnumSingleton.INSTANCE;
        Constructor<EnumSingleton> dc = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
        dc.setAccessible(true);
        EnumSingleton singleCopy = dc.newInstance();

        System.out.println(single);
        System.out.println(singleCopy);
        System.out.println(single == singleCopy);
    }
}
複製代碼

至於爲什麼反射獲取的構造方法傳入String、int參數,需要通過Jad反編譯來查看。不能傳入空參構造,否則出現的是NoSuchMethodException

出現源碼中拋出的異常IllegalArgumentException

 

 

程序最終拋出:java.lang.IllegalArgumentException: Cannot reflectively create enum objects異常


總結

在JDK中,java.lang.Runtime就是經典的單例模式

 

 

singleCopy = dc.newInstance();

    System.out.println(single);
    System.out.println(singleCopy);
    System.out.println(single == singleCopy);
}
複製代碼

 


> 至於爲什麼反射獲取的構造方法傳入String、int參數,需要通過[Jad](http://java-decompiler.github.io/)反編譯來查看。**不能傳入空參構造**,否則出現的是`NoSuchMethodException`

出現源碼中拋出的異常`IllegalArgumentException`

[外鏈圖片轉存中...(img-v08jSJlp-1592189800203)]

> 程序最終拋出:java.lang.IllegalArgumentException: Cannot reflectively create enum objects異常



--------------------------------------------

## 總結

在JDK中,`java.lang.Runtime`就是經典的單例模式

[外鏈圖片轉存中...(img-DlGafTDn-1592189800204)]

掌握這樣一些單例模式的奇淫技巧,在歷經反射的重重爆破之後,相信你會對單例模式有新的瞭解!

 

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