Java設計模式之餓漢式單例模式

Java設計模式之餓漢式單例模式

 

public class HungrySingleton {
    private HungrySingleton(){}

    private final static HungrySingleton hungrySingleton=new HungrySingleton();

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

餓漢式單例模式的優點是寫法簡單,類加載的時候就完成了初始化,也可以避免多線程問題。不足之處是類加載就完成了初始化,但是如果後面不用初始化好的對象,可能造成資源浪費。

 

問題:獲取到的hungrySingleton對象經序列化保存到文件後,再反序列化得到的對象與原對象是同一個嗎?下面開始測試:

//首先將HungrySingleton序列化
public class HungrySingleton implements Serializable
//調用
public class Test {
    public static void main(String[] a){
        HungrySingleton hungrySingleton=HungrySingleton.getInstance();
        try {
            //將對象寫入文件
            ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("singleton_file"));
            oos.writeObject(hungrySingleton);
            File file=new File("singleton_file");
            //從文件中獲取對象
            ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));
            HungrySingleton hungrySingleton2= (HungrySingleton) ois.readObject();
            //比較兩個對象是否相同
            System.out.println(hungrySingleton);
            System.out.println(hungrySingleton2);
            System.out.println(hungrySingleton == hungrySingleton2);
        } catch (Exception e) { e.printStackTrace(); }
    }
}

//結果
com.zk.javatest.singleton.lazy_singleton.HungrySingleton@135fbaa4
com.zk.javatest.singleton.lazy_singleton.HungrySingleton@568db2f2
false

從上面的結果來看,兩個對象不相同。這違反了單例模式,這個問題怎麼解決呢?解決方法也比較簡單。如下: 


public class HungrySingleton implements Serializable{
    private HungrySingleton(){}

    private final static HungrySingleton hungrySingleton=new HungrySingleton();

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

//在類裏面增加方法readResolve
    private Object readResolve(){
        return  hungrySingleton;
    }
}

//結果
com.zk.javatest.singleton.lazy_singleton.HungrySingleton@135fbaa4
com.zk.javatest.singleton.lazy_singleton.HungrySingleton@135fbaa4
true

這樣,序列化以後的對象與之前的對象相同,問題解決了。雖然解決問題的方法很簡單,但是原理我們要弄清楚,爲什麼要這樣解決?

//核心方法readObject()
 HungrySingleton hungrySingleton2= (HungrySingleton) ois.readObject();


public final Object readObject() throws IOException, ClassNotFoundException {
        
            Object var4;
            try {
            //核心方法readObject0
                Object var2 = this.readObject0(false);            
            } 
            return var4;
        }
    }

private Object readObject0(boolean var1){
      case 115:
      //核心方法readOrdinaryObject
      var4 = this.checkResolve(this.readOrdinaryObject(var1));
      return var4;
}


private Object readOrdinaryObject(boolean var1) throws IOException {
        
            if (var3 != String.class && var3 != Class.class && var3 != ObjectStreamClass.class) {
                Object var4;
               
                //核心代碼,var2.isInstantiable()爲true,執行var2.newInstance() 
                //通過反射創建新的對象,這也解釋了兩個對象不相同的原因。
                var4 = var2.isInstantiable() ? var2.newInstance() : null;                                
        }

        //如果HungrySingleton類裏面實現了readResolve方法,則通過返回來調用此方法。
//由於readResolve方法是直接return  hungrySingleton,這樣就保證了兩個對象相同。
        if (var4 != null && this.handles.lookupException(this.passHandle) == null && var2.hasReadResolveMethod()){
            Object var6 = var2.invokeReadResolve(var4);
        }
}


//最後,看一下這個readResolve方法名聲明的地方,可以知道這個名字是不能修改的。
   ObjectStreamClass.this.readResolveMethod = ObjectStreamClass.getInheritableMethod(var1, "readResolve", (Class[])null, Object.class);

 在整個流程中可以看到,雖然最後返回的是同一個對象,但是中間卻依然重新創建了一個不同的實例,只不過被後來的對象覆蓋掉了。

下面來看另外一個問題,雖然單例類的構造器是私有的,外面無法new出對象,但是能否通過反射並修改構造器的權限,然後獲取對象呢?我們來試試。


//首先通過單例模式獲取對象
        HungrySingleton instance=HungrySingleton.getInstance();
        try {
            Class objectClass=HungrySingleton.class;
            //通過反射獲取構造器
            Constructor constructor=objectClass.getDeclaredConstructor();
            //修改構造器的權限
            constructor.setAccessible(true);
            //通過構造器獲取新的對象
            HungrySingleton newInstance= (HungrySingleton) constructor.newInstance();
            //比較兩個對象是否相同
            System.out.println(instance);
            System.out.println(newInstance);
            System.out.println(instance == newInstance  );
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

//結果
com.zk.javatest.singleton.lazy_singleton.HungrySingleton@1540e19d
com.zk.javatest.singleton.lazy_singleton.HungrySingleton@677327b6
false

由此可見,通過反射出來的構造器也可以獲取對象,那麼如何來防止這種反射攻擊呢? 


public class HungrySingleton implements Serializable{
    private HungrySingleton(){
    //在這裏增加判斷,來對反射進行防禦編程
        if (hungrySingleton!=null){
            throw new RuntimeException("單例模式的構造器禁止反射");
        }
    }

    private final static HungrySingleton hungrySingleton=new HungrySingleton();

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

    private Object readResolve(){
        return  hungrySingleton;
    }
}

運行結果:

 

如果是懶漢式加載,一旦多線程,就和順序有關,如果反射調用先執行,就會獲取新的對象,後面再通過單例獲取的就是另外一個對象。因此,懶漢式單例模式,無法完全避免反射攻擊,這點要注意。

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