教你破壞單例模式

前言

以前在學習單例模式的時候從沒考慮過安全的問題,一直覺得單例是無懈可擊的,今天我來教你如何破壞單例模式以及應對方法。java

首先來看看下面這個常見的單例寫法面試

public class Singleton(
    
    private Singleton(){}
    
    private static class SingletonInstance{
        private static final Singleton instance=new Singleton();
    }
    
    public static Singleton getInstance(){
        return SingletonInstance.instance;
    }
)

看起來無懈可擊,那麼真的是這樣嗎?安全

咱們知道要破壞單例,則必須建立對象,那麼咱們順着這個思路走,建立對象的方式無非就是new,clone,反序列化,以及反射學習

單例模式的首要條件就是構造方法私有化,因此new這種方式去破壞單例的可能性是不存在的
要調用clone方法,那麼必須實現Cloneable接口,可是單例模式是不能實現這個接口的,所以排除這種可能性。
所以咱們本篇來討論一下反序列化反射如何對單例模式進行破壞。測試

使用反序列化破壞單例模式

序列化是破壞單例模式的一大利器。其與克隆性質有些類似,須要類實現序列化接口,相比於克隆,實現序列化在實際操做中更加不可避免,有些類,它就是必定要序列化。this

下面咱們來作個測試,在上面的單例模式中實現序列化接口,以下spa

public class Singleton implements Serializable {

而後咱們對拿到的對象進行序列化和反序列進行測試debug

public class Test {  
    public static void main(String[] args) throws Exception {  
        //序列化  
        Singleton instance1 = Singleton.getInstance();  
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));  
        objectOutputStream.writeObject(instance1);  
        //反序列化  
        File file = new File("tempFile");  
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));  
        Singleton instance2 = (Singleton) objectInputStream.readObject();  
        System.out.println(instance1 == instance2);  
    }  
}

執行結果爲 3d

image.png

經過對Singleton的序列化與反序列化獲得的對象是一個新的對象,這就破壞了Singleton的單例性。code

image.png

接下來咱們來試着打個斷點debug一下

image.png

能夠看到進入了readObject0這個方法裏,咱們進去看看

image.png

繼續下一步會走到readOrdinaryObject方法中,能夠看到其實反序列化底層也是使用反射幫咱們建立了一個新的對象

image.png

那是否是咱們就不能阻止單例被破壞了呢?並非!

如今咱們在Singleton類中加上了一個readResolve方法,該方法返回了INSTANCE實例,而後從新執行一下:

public class Singleton implements Serializable {  
  
    private Singleton() {  
    }  
  
    private static class SingletonInstance {  
        private static final Singleton INSTANCE = new Singleton();  
   }   
    public static Singleton getInstance() {  
        return SingletonInstance.INSTANCE;  
   }  
    private Object readResolve() {  
        return SingletonInstance.INSTANCE;  
   }  
 
}

image.png

結果居然爲true,也就是說序列化和反序列出來的是同一個對象!

image.png

那這究竟是什麼原理,咱們來看看剛纔的readOrdinaryObject方法:

image.png

看到上面應該很清楚了,在條件判斷中 desc.hasReadResolveMethod()會判斷是否有readResolve()方法,若是有的話會經過desc.invokeReadResolve(obj)去反射調用該方法,返回的就是同一個對象。

看到這裏小夥伴們應該明白了,總結一句話就是:若是想要防止單例被反序列化破壞。就讓單例類實現readResolve()方法。

使用反射破壞單例模式

說完反序列化破壞單例,那如今咱們來看看反射如何破壞單例模式:

public class Test {  
    public static void main(String[] args) throws Exception {  
  
      Singleton instance1 = Singleton.getInstance();  
      //經過反射建立對象  
      Class<Singleton> singletonClass = Singleton.class;  
      Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor(); 
      //暴力破解私有構造器
      constructor.setAccessible(true);  
      Singleton instance2 = constructor.newInstance();  
     
      System.out.println(instance1 == instance2);  
  }  
}

image.png

執行結果爲 false,也就是說經過反射也可以破壞單例模式

咱們如何應對呢?
即使是經過反射來建立實例,也是調用類中的構造器來實現的,因此咱們能夠在構造器中作文章。
改造Singleton類中的私有構造器以下:

public class Singleton implements Serializable {  
  
    private Singleton() {  
        if (SingletonInstance.INSTANCE != null) {  
            throw new RuntimeException("不容許反射調用構造器");  
      }  
    }    
    private static class SingletonInstance {  
        private static final Singleton INSTANCE = new Singleton();  
    }    
    public static Singleton getInstance() {  
        return SingletonInstance.INSTANCE;  
    }    
    private Object readResolve() {  
        return SingletonInstance.INSTANCE;  
    }  
}

執行結果:
image.png

很顯然報異常了,這樣便防止了這種方法實現的單例模式被反射破壞。

image.png

餓漢式實現的單例模式均可以這樣來防止單例模式被反射破壞。
懶漢式實現的單例模式是不能夠防止被反射破壞的。
如今咱們用雙重檢查鎖式實現的單例模式來進行測試:

public class Singleton {
    private static volatile Singleton instance ;

    private Singleton(){
        if(instance != null){
            throw new RuntimeException("不容許反射調用構造器");
        }
    }

    public static Singleton getInstance(){
        if(instance == null){
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

image.png

從新執行一下,結果仍是同樣,那這樣就沒問題了嗎?不!

如今咱們修改一下測試類:

public class Test {
    public static void main(String[] args) throws Exception {
        //經過反射建立單例對象
        Class<Singleton> singletonClass = Singleton.class;
        Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();
        //獲取單例對象
        Singleton instance1 = Singleton.getInstance();
        
        System.out.println(instance1 == instance2);
    }
}

調整一下順序,如今咱們先使用反射建立對象,再調用單例的getInstance()方法,結果以下:

image.png

咱們把經過反射建立實例和調用靜態方法getInstance()得到實例的位置互換了,因此一開始經過反射建立實例調用構造器,此時構造器中的判斷instance != null是無用的,因此這種方法是不適用懶漢式實現的單例模式來防止被反射破壞的。

總結:若是從此須要本身手動實現一個單例的話,能夠選擇 構造器判斷 + 實現 readResolve() 方法的方式
來防止單例被破壞。

image.png

那麼有沒有更簡單的方法呢?答案是有的!

若是不想在構造器內部加判斷,也不想寫readResolve()方法,那你能夠選擇使用枚舉來實現單例模式

使用枚舉實現單例

在StakcOverflow中,有一個關於 What is an efficient way to implement a singleton pattern in Java? 的討論:

image.png

如上圖,得票率最高的回答是:使用枚舉。

回答者引用了Joshua Bloch大神在《Effective Java》中明確表達過的觀點:

使用枚舉實現單例的方法雖然尚未普遍採用,可是單元素的枚舉類型已經成爲實現Singleton的最佳方法。

接下來咱們來看看如何使用枚舉實現單例:

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

能夠看到相比雙重檢查等單例模式,使用枚舉實現的單例模式更加優雅,那麼上面這個代碼是安全的嗎,仍是用原來的測試代碼來測試一下:

public class Test {
    public static void main(String[] args) throws Exception {

        Singleton instance1 = Singleton.getInstance();

        //經過反射建立對象
        Class<Singleton> singletonClass = Singleton.class;
        Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();
        
        System.out.println(instance1 == instance2);
    }
}

image.png

結果會報 Exception in thread "main" java.lang.NoSuchMethodException

image.png

簡單來講就是由於SingletonClass.getDeclaredConstructors()獲取全部構造器,會發現並無咱們所設置的無參構造器,只有一個參數爲(String.class,int.class)構造器,由於一旦一個類聲明爲枚舉,實際上就是繼承了Enum,來看看Enum類源碼:

public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
        private final String name;
        public final String name() {
            return name;
        }
        private final int ordinal;
        public final int ordinal() {
            return ordinal;
        }
        protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
        //餘下省略

看下Enum源碼就明白,這兩個參數是nameordial兩個屬性,由於繼承了父類構造器,因此在剛纔的測試中才會找不到無參構造器,那麼是否是咱們去調用父類的構造器就能夠了呢?咱們來測試一下:

public class Test {
    public static void main(String[] args) throws Exception {

        Singleton instance1 = Singleton.getInstance();

        //經過反射建立對象
        Class<Singleton> singletonClass = Singleton.class;
        //調用父類構造器
        Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();

        System.out.println(instance1 == instance2);
    }
}

注意:咱們在上面經過singletonClass.getDeclaredConstructor(String.class, int.class)來調用父類構造器,來看下執行結果:

image.png

來看看在哪裏拋出的異常:

image.png

總結來講就是反射在經過newInstance建立對象時,會檢查該類是否ENUM修飾,若是是則拋出異常,反射失敗。因此枚舉是不怕發射攻擊的。

枚舉和反序列化

那枚舉又是如何避免被反序列化來建立新對象的呢?

枚舉對象的序列化、反序列化有本身的一套機制。序列化時,僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是經過java.lang.Enum的valueOf()方法來根據名字查找枚舉對象。

下面分析一下valueOf源碼:

image.png

再來看看enumConstantDirectory()源碼:

image.png

繼續看getEnumConstantsShared()源碼:

image.png

getEnumConstantsShared()方法獲取枚舉類的values()方法,而後獲得枚舉類所建立的全部枚舉對象。

每一個枚舉對象都有一個惟一的name屬性。序列化只是將name屬性序列化,在反序列化的時候,經過建立一個Map(key,value),搭建起name和與之對應的對象之間的聯繫,而後經過索引key來得到枚舉對象

總的來講就是枚舉在反序列化的過程當中並無建立新的對象,而經過name屬性拿到原有的對象,所以保證了枚舉類型實現單例模式的序列化安全。

總結

若是從此要本身手動實現一個單例模式首先推薦使用枚舉來實現,在面試被問到單例模式的時候也能夠和麪試官吹吹牛逼了,今天就暫時學習到這裏,若是有什麼不對的地方請多多指教。
image.png

相關文章
相關標籤/搜索