天天看點

單例模式中的那些坑

前言

什麼是單例模式

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。
這種類型的設計模式屬于建立型模式,它提供了一種建立對象的最佳方式。

這種模式涉及到一個單一的類,該類負責建立自己的對象,同時確定隻有單個對象被建立。
這個類提供了一種通路其唯一的對象的方式,可以直接通路,不需要執行個體化該類的對象。

注意:

1、單例類隻能有一個執行個體。
2、單例類必須自己建立自己的唯一執行個體。
3、單例類必須給所有其他對象提供這一執行個體。
# 摘自菜鳥教程:https://www.runoob.com/design-pattern/singleton-pattern.html           

單例模式又分 餓漢模式,懶漢模式

本片文章主要講解懶漢模式

懶漢模式

  • 首先來看一下它的定義
懶漢模式:延遲加載,隻有在真正使用的時候,才開始執行個體化.           

實作方式

1.雙檢鎖

private static volatile LazySingleton instance;

    private LazySingleton(){

        if(instance != null){
            throw new RuntimeException("不允許通過反射擷取");
        }
    }


    public static  LazySingleton getInstance() {
        //第一次檢查
        if (instance == null) {
            //擷取鎖
            //第一次通路,多個線程同時擠進來,隻有一個線程可以擷取鎖
            synchronized (LazySingleton.class){
                //第一個線程進入 此處為空,進入if并建立對象并傳回,之後獲得鎖的線程此處判斷不為空直接傳回
                if(instance == null) {
                    //執行構造方法
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }           

問題1:此處為什麼使用volatile.

private static volatile LazySingleton instance;

建立對象是非原子性操作,有三個過程 配置設定空間,初始化對象,指派,其中2,3步其中可能會發生指令重排現象.

  • 代碼示例
    //假設我們有如下語句
    Holder holder = new Holder();
    //則實際執行的操作如下
    tmpRef=allocate(Holder.class)//1.配置設定空間
    invokeConstructor(tmpRef)//2. 執行構造函數
    holder = tmpRef //3.指派           
  • 隻有在初始化對象的那一步才會真正執行構造方法
  • 編譯器(JIT),CPU有可能對指令進行重排序,導緻使用到尚未初始化的執行個體,可以通過添加volatile關鍵字進行修飾,對于volatile修飾的字段,可以防止指令重排.

    問題2:構造方法的反射判斷

對于構造方法聲明為private,可以防止直接new對象,但是阻止不了反射來擷取對象,進而破壞單例.

private LazySingleton(){
        if(instance != null){
            throw new RuntimeException("不允許通過反射擷取");
        }
}           

以上代碼并不能完美的阻止反射,如果從一開始就直接使用反射而不直接去調用提供的建立方法 就會被破解

Class<LazySingleton> lazySingletonClass = LazySingleton.class;
        Constructor<LazySingleton> constructor = lazySingletonClass.getDeclaredConstructor();
        // 暴力反射
        constructor.setAccessible(true);
        // 從一開始就不使用給定的方法來建立單例
        //LazySingleton instance = LazySingleton.getInstance();
        LazySingleton lazySingleton = constructor.newInstance();
        LazySingleton lazySingleton1 = constructor.newInstance();
        System.out.println(lazySingleton);
        System.out.println(lazySingleton1);           

執行結果

com.leetao.singleton.LazySingleton@511d50c0
com.leetao.singleton.LazySingleton@60e53b93           

完全是兩個對象...

  • 難道就真的任反射随意宰割了? 别着急,下面會通過靜态内部類的方式來介紹如何解決的
  • 在這之前,還是先得來了解一下序列化破壞反射吧
序列化破壞
  • 代碼執行個體
//序列化的對象已實作Serializable接口
    
        //記憶體輸出流,此處也可以使用檔案輸出流(持久化)來代替
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        //記憶體輸入流
    
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        //擷取單例
        LazySingleton instance = LazySingleton.getInstance();
        //存入對象輸出中
        objectOutputStream.writeObject(instance);
        objectOutputStream.flush();
        objectOutputStream.close();
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        //對象輸入流
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        LazySingleton lazySingleton = ((LazySingleton) objectInputStream.readObject());
        //方法建立的單例
        System.out.println(instance);
        //序列化之後的單例
        System.out.println(lazySingleton);           

運作結果

com.leetao.singleton.LazySingleton@6f94fa3e
com.leetao.singleton.LazySingleton@4e50df2e           

也不是同一個...

  • 關于序列化對象之後為什麼不是同一個的問題
  • 因為使用了預設的序列化機制,他會直接從位元組流中拿資料,并不會去調構造函數來進行初始化
附1:JAVA序列化過程
1.将對象執行個體相關的類中繼資料輸出。
2.遞歸地輸出類的超類描述直到不再有超類。
3.類中繼資料完了以後,開始從最頂層的超類開始輸出對象執行個體的實際資料值。
4.從上至下遞歸輸出執行個體的資料           

腳注: 預設的序列化機制會将對象所有實作Serializable接口的-内容-全部序列化,序列化過程會讀取内容的位元組流資料,會通過此産生新的對象,并不是通過構造函數來制造新的對象(網上很多文章都說序列化通過構造函數來建立對象,其實并不是!!)。

原型模式中的深克隆也就是通過此機制來實作的.

腳注所示内容代表的是:對象繼承的類以及超類..、成員變量中的引用變量

  • 那麼單例中的序列化破壞如何解決

    官方答案

單例模式中的那些坑
  • 可以通過寫入readResovler() throws ObjectStreamException 方法來實作自定義的序列化機制

    代碼

/**
      * 自定義的序列化機制
      */
    private Object readResolve() throws ObjectStreamException{
        //直接傳回單例
        return LazySingleton.getInstance();
    }           

再次運作結果

com.leetao.singleton.LazySingleton@6f94fa3e
com.leetao.singleton.LazySingleton@6f94fa3e           

序列化破壞的問題完美解決

2. 靜态内部類

1.本質上是利用類加載器機制來保證線程安全
2.隻有在實際使用的時候,才會觸發類的初始化,是以也是懶加載的一種形式
3.借助于jvm類加載機制,保證執行個體的唯一性.           

何為類加載

# 類加載過程
1. 加載: 将位元組碼資料從不同的資料源讀取到 JVM 中,并映射為 JVM 認可的資料結構(Class 對象)
2. 連接配接: a.校驗,b.準備(給類的靜态成員變量賦預設值),c.解析
3. 初始化:給類的靜态變量賦初值

# 注意:隻有在真正使用對應的類是,才會觸發初始化 如
    1. 目前類是啟動類(main方法所在的類).
    2. 直接進行new操作.
    3. 通路靜态字段(final修飾的靜态字面量除外).
    4. 通路靜态方法.
    5. 用反射通路類.
    6. 初始化此類的子類.
    $. 後續會出關于類加載相關的文章           

前面提到反射破壞的問題,在靜态内部類中可以這樣解決

public class LeeFactory {
    private LeeFactory(){
        //通過靜态類加載機制破解反射破壞
       if(LeeFactoryHolder.LEE_FACTORY!=null){
            throw new UnsupportedOperationException("非法反射不予允許");
        }
    }
    /**
     * 靜态内部類
     */
    private static class LeeFactoryHolder{
        private static final LeeFactory LEE_FACTORY  = new LeeFactory();
    }

    public static LeeFactory getInstance(){
        return LeeFactoryHolder.LEE_FACTORY;
    }

}           

以上代碼可以完美解決反射破壞,如果直接通過getInstance()的方式來擷取對象的話.第一次調用才會觸發類初始化和構造方法.之後的調用直接拿資料

而第一次調用反射會觸發兩次兩次構造方法,

1.構造方法中if中的判斷會調用一次(因為通路類字段會涉及到類初始化,類初始化調用了構造方法)

2.反射建立對象本身會調用一次構造方法,此時靜态内部類字段因為初始化已經存在值了(判斷有值,抛出異常)

還有一個特點

private static final LeeFactory LEE_FACTORY = new LeeFactory();

為什麼加final

1.因為一旦被指派便無法在修改,即使是反射也不能(也是因為這個原因才使用final)

2.被final修飾的字段會在完全初始化後才會對其他線程可見

說到這裡不得不說為什麼不用volatile

final和volatile 
volatile修飾的字段不但可以防止重排序,還可以直接在主存更新,讓其他線程同步更新
但是它阻止不了反射對其重新指派,如果使用反射對内部類字段指派為null,會導緻其他正常調用的代碼出現問題.

而final修飾的類,會在完全初始化後才會對其他線程可見,而且不能被反射破壞,正好符合我們的需求           

如果對final感興趣的同學,可以閱讀

https://zhuanlan.zhihu.com/p/100536345

了解更多

結尾:淺談枚舉單例

枚舉本質上是一個不可變類 ,它的成員全部為類字段.它不可以被反射所破壞,同時還擁有自己的序列化機制.可以說是完美的單例.

參考

Java序列化機制

https://www.iteye.com/blog/bingobird-867950

final特征

文中所述内容,如有錯誤,歡迎指正.

忌妒别人,不會給自己增加任何的好處,忌妒别人,也不可能減少别人的成就。

菅江晖