單例模式的研究重點有以下幾個:
- 構造私有,提供靜態輸出接口
- 線程安全,確保全局唯一
- 延遲初始化
- 防止反射攻擊
- 防止序列化破壞單例模式
上一節《單例設計模式實現總結》,我們使用餓漢式、雙重鎖檢查、靜態內部類、枚舉類實踐了前3條。然而光併發安全並不能保證唯一實例,反射和序列化可以破壞單例模式。
public class ReflectSingleton {
private final static ReflectSingleton instance = new ReflectSingleton();
private ReflectSingleton() {
}
public static ReflectSingleton getInstance() {
return instance;
}
}
本文中採用餓漢單例模式作爲最初代碼,演示如何避免反射和序列化破壞單例模式。
防止反射攻擊
使用反射攻擊單例模式
public class Client {
public static void main(String[] args) throws Exception {
// 通過全局訪問方法創建實例
ReflectSingleton instance = ReflectSingleton.getInstance();
// 通過反射創建實例
Constructor constructor = ReflectSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
ReflectSingleton newInstance = (ReflectSingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
使用反射時,需要添加構造器權限,否則會拋異常。
Exception in thread "main" java.lang.IllegalAccessException: Class com.lzp.java.concurrent.singleton.destroysingleton.Client can not access a member of class com.lzp.java.concurrent.singleton.destroysingleton.ReflectSingleton with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
at java.lang.reflect.Constructor.newInstance(Constructor.java:413)
at com.lzp.java.concurrent.singleton.destroysingleton.Client.main(Client.java:16)
運行結果:
com.lzp.java.concurrent.singleton.destroysingleton.ReflectSingleton@355da254
com.lzp.java.concurrent.singleton.destroysingleton.ReflectSingleton@4dc63996
false
改進措施:反射防禦
抵禦這種攻擊,可以在構造器中添加反射防禦代碼,讓它在被要求創建第二個實例時拋出異常。
private ReflectSingleton() {
if (instance != null) {
throw new RuntimeException("禁止反射調用創建多個實例");
}
}
運行結果:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.lzp.java.concurrent.singleton.destroysingleton.Client.main(Client.java:18)
Caused by: java.lang.RuntimeException: 禁止反射調用創建多個實例
需要注意的是,在構造器中添加反射防禦代碼,僅適用於基於類初始化加載的單例實現,即餓漢式和靜態內部類實現。對於雙重鎖檢查不會出現反射攻擊的情況。
防止序列化破壞單例模式
反序列化問題
public class Client2 {
public static void main(String[] args) throws Exception {
// 使用全局訪問方法創建實例
SerializeSingleton instance = SerializeSingleton.getInstance();
// 寫出對象到項目目錄下singleton.txt文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
oos.writeObject(instance);
// 讀入對象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.txt"));
SerializeSingleton newInstance = (SerializeSingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
運行結果:
com.lzp.java.concurrent.singleton.destroysingleton.SerializeSingleton@4b1210ee
com.lzp.java.concurrent.singleton.destroysingleton.SerializeSingleton@27973e9b
false
改進措施:添加readResolve()方法
private Object readResolve() {
return instance;
}
運行結果:
com.lzp.java.concurrent.singleton.destroysingleton.SerializeSingleton@4b1210ee
com.lzp.java.concurrent.singleton.destroysingleton.SerializeSingleton@4b1210ee
true
爲什麼是readResolve(),而不是其他方法?
此時可以對源碼做單步調試。
// 核心語句
SerializeSingleton newInstance = (SerializeSingleton) ois.readObject();
// ObjectInputStream
public final Object readObject(){
...
try {
Object obj = readObject0(false);
.....
}
}
readObject方法內部調用readObject0方法。
// ObjectInputStream
private Object readObject0(boolean unshared) throws IOException {
......
try {
switch (tc) {
........
case TC_OBJECT: // 如果是讀取對象Object
return checkResolve(readOrdinaryObject(unshared));
.........
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
定位到關鍵方法readOrdinaryObject()。
// ObjectInputStream
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
.......
Object obj;
try {
// 注:如果爲true,通過反射創建新的實例
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
......
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
// 內部核心語句:return readResolveMethod.invoke(obj, (Object[]) null);
// 反射創建原實例
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// 替換對象
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
/**
* 如果類是可序列化的,返回true
*/
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
/**
* 如果類是可序列化的,並且定義了readResolve()方法,返回true;否則返回false
*/
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
通過調試,我們可以看出,調readObject()方法反序列化的過程中,總會創建一個新的實例。如果SerializeSingleton類中定義了readResolve方法,就通過反射創建原實例,返回時覆蓋之前創建的實例。否則,返回新的實例。
通過底層代碼分析,我們便清楚了爲什麼用的是readResolve方法,而不是其他。