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