很久沒有寫過接地氣的東西了,今天隨便寫一個非常基礎的。其實這篇文章也可以叫做《Java單例的破壞與防御方法》,無所謂了。
講解Java單例實現方式及其原理的文章數不勝數,本文就不再多廢話。在實際生產環境中,以下3種方式最常用,先復習一下。看官也可以試試能不能不參考任何資料,將下面的問題都回答正確。
Java單例的三種經典實現
雙重檢查鎖(DCL)
public class DoubleCheckLockSingleton {
private static volatile DoubleCheckLockSingleton instance;
private DoubleCheckLockSingleton() {}
public static DoubleCheckLockSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckLockSingleton.class) {
if (instance == null) {
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
public void tellEveryone() {
System.out.println("This is a DoubleCheckLockSingleton " + this.hashCode());
}
}
- volatile關鍵字在此處起了什么作用?
- 為何要執行兩次
instance == null
判斷?
靜態內部類
public class StaticInnerHolderSingleton {
private static class SingletonHolder {
private static final StaticInnerHolderSingleton INSTANCE = new StaticInnerHolderSingleton();
}
private StaticInnerHolderSingleton() {}
public static StaticInnerHolderSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
public void tellEveryone() {
System.out.println("This is a StaticInnerHolderSingleton" + this.hashCode());
}
}
- 這種方式是通過什么機制保證線程安全性與延遲加載的?(注意,這是Java單例的兩大要點,必須保證)
枚舉
public enum EnumSingleton {
INSTANCE;
public void tellEveryone() {
System.out.println("This is an EnumSingleton " + this.hashCode());
}
}
- Java枚舉的本質是?
- 這種方式又是通過什么機制保證線程安全性與延遲加載的?
復習完了。
在Java圣經《Effective Java》中,Joshua Bloch大佬如是說:
A single-element enum type is often the best way to implement a singleton.
為什么說枚舉是(一般情況下)最好的Java單例實現呢?他也做出了簡單的說明:
It is more concise, provides the serialization machinery for free, and provides an ironclad guarantee against multiple instantiation, even in the face of sophisticated serialization or reflection attacks.
大意就是,枚舉單例可以有效防御兩種破壞單例(即使單例產生多個實例)的行為:反射攻擊與序列化攻擊(雖然我之前講過“簡單易懂的現代魔法”Unsafe,但它過于邪門歪道了,不算數)。言外之意就是前兩種單例方式都會被破壞。那么我們就拿平時最常用的雙重檢查鎖方式開刀來試試看。
如何破壞一個單例
反射攻擊
直接上代碼:
public class SingletonAttack {
public static void main(String[] args) throws Exception {
reflectionAttack();
}
public static void reflectionAttack() throws Exception {
Constructor constructor = DoubleCheckLockSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
DoubleCheckLockSingleton s1 = (DoubleCheckLockSingleton)constructor.newInstance();
DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)constructor.newInstance();
s1.tellEveryone();
s2.tellEveryone();
System.out.println(s1 == s2);
}
}
執行結果如下:
This is a DoubleCheckLockSingleton 1368884364
This is a DoubleCheckLockSingleton 401625763
false
這種方法非常簡單暴力,通過反射侵入單例類的私有構造方法并強制執行,使之產生多個不同的實例,這樣單例就被破壞了。要防御反射攻擊,只能在單例構造方法中檢測instance是否為null,如果已不為null,就拋出異常。顯然雙重檢查鎖實現無法做這種檢查,靜態內部類實現則是可以的。
注意,不能在單例類中添加類初始化的標記位或計數值(比如boolean flag
、int count
)來防御此類攻擊,因為通過反射仍然可以隨意修改它們的值。
序列化攻擊
這種攻擊方式只對實現了Serializable接口的單例有效,但偏偏有些單例就是必須序列化的。現在假設DoubleCheckLockSingleton類已經實現了該接口,上代碼:
public class SingletonAttack {
public static void main(String[] args) throws Exception {
serializationAttack();
}
public static void serializationAttack() throws Exception {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serFile"));
DoubleCheckLockSingleton s1 = DoubleCheckLockSingleton.getInstance();
outputStream.writeObject(s1);
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("serFile")));
DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)inputStream.readObject();
s1.tellEveryone();
s2.tellEveryone();
System.out.println(s1 == s2);
}
}
執行結果如下:
This is a DoubleCheckLockSingleton 777874839
This is a DoubleCheckLockSingleton 254413710
false
為什么會發生這種事?長話短說,在ObjectInputStream.readObject()方法執行時,其內部方法readOrdinaryObject()中有這樣一句話:
obj = desc.isInstantiable() ? desc.newInstance() : null;
其中desc是類描述符。也就是說,如果一個實現了Serializable/Externalizable接口的類可以在運行時實例化,那么就調用newInstance()方法,使用其默認構造方法反射創建新的對象實例,自然也就破壞了單例性。要防御序列化攻擊,就得將instance聲明為transient,并且在單例中加入以下語句:
private Object readResolve() {
return instance;
}
這是因為在上述readOrdinaryObject()方法中,會通過衛語句desc.hasReadResolveMethod()
檢查類中是否存在名為readResolve()的方法,如果有,就執行desc.invokeReadResolve(obj)
調用該方法。readResolve()會用自定義的反序列化邏輯覆蓋默認實現,因此強制它返回instance本身,就可以防止產生新的實例。
枚舉單例的防御機制
對反射的防御
我們直接將上述reflectionAttack()方法中的類名改成EnumSingleton并執行,會發現報如下異常:
Exception in thread "main" java.lang.NoSuchMethodException: me.lmagics.singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at me.lmagics.singleton.SingletonAttack.reflectionAttack(SingletonAttack.java:35)
at me.lmagics.singleton.SingletonAttack.main(SingletonAttack.java:19)
這是因為所有Java枚舉都隱式繼承自Enum抽象類,而Enum抽象類根本沒有無參構造方法,只有如下一個構造方法:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
那么我們就改成獲取這個有參構造方法,即:
Constructor constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
結果還是會拋出異常:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at me.lmagics.singleton.SingletonAttack.reflectionAttack(SingletonAttack.java:38)
at me.lmagics.singleton.SingletonAttack.main(SingletonAttack.java:19)
來到Constructor.newInstance()方法中,有如下語句:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
可見,JDK反射機制內部完全禁止了用反射創建枚舉實例的可能性。
對序列化的防御
如果將serializationAttack()方法中的攻擊目標換成EnumSingleton,那么我們就會發現s1和s2實際上是同一個實例,最終會打印出true。這是因為ObjectInputStream類中,對枚舉類型有一個專門的readEnum()方法來處理,其簡要流程如下:
- 通過類描述符取得枚舉單例的類型EnumSingleton;
- 取得枚舉單例中的枚舉值的名字(這里是INSTANCE);
- 調用Enum.valueOf()方法,根據枚舉類型和枚舉值的名字,獲得最終的單例。
這種處理方法與readResolve()方法大同小異,都是以繞過反射直接獲取單例為目標。不同的是,枚舉對序列化的防御仍然是JDK內部實現的。
綜上所述,枚舉單例確實是目前最好的單例實現了,不僅寫法非常簡單,并且JDK能夠保證其安全性,不需要我們做額外的工作。