很久沒有寫過接地氣的東西了载佳,今天隨便寫一個(gè)非吵词拢基礎(chǔ)的。其實(shí)這篇文章也可以叫做《Java單例的破壞與防御方法》蔫慧,無所謂了挠乳。
講解Java單例實(shí)現(xiàn)方式及其原理的文章數(shù)不勝數(shù),本文就不再多廢話姑躲。在實(shí)際生產(chǎn)環(huán)境中睡扬,以下3種方式最常用,先復(fù)習(xí)一下黍析÷袅看官也可以試試能不能不參考任何資料,將下面的問題都回答正確阐枣。
Java單例的三種經(jīng)典實(shí)現(xiàn)
雙重檢查鎖(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關(guān)鍵字在此處起了什么作用马靠?
- 為何要執(zhí)行兩次
instance == null
判斷?
靜態(tài)內(nèi)部類
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());
}
}
- 這種方式是通過什么機(jī)制保證線程安全性與延遲加載的蔼两?(注意甩鳄,這是Java單例的兩大要點(diǎn),必須保證)
枚舉
public enum EnumSingleton {
INSTANCE;
public void tellEveryone() {
System.out.println("This is an EnumSingleton " + this.hashCode());
}
}
- Java枚舉的本質(zhì)是额划?
- 這種方式又是通過什么機(jī)制保證線程安全性與延遲加載的妙啃?
復(fù)習(xí)完了。
在Java圣經(jīng)《Effective Java》中俊戳,Joshua Bloch大佬如是說:
A single-element enum type is often the best way to implement a singleton.
為什么說枚舉是(一般情況下)最好的Java單例實(shí)現(xiàn)呢彬祖?他也做出了簡單的說明:
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.
大意就是,枚舉單例可以有效防御兩種破壞單例(即使單例產(chǎn)生多個(gè)實(shí)例)的行為:反射攻擊與序列化攻擊(雖然我之前講過“簡單易懂的現(xiàn)代魔法”Unsafe品抽,但它過于邪門歪道了储笑,不算數(shù))。言外之意就是前兩種單例方式都會(huì)被破壞圆恤。那么我們就拿平時(shí)最常用的雙重檢查鎖方式開刀來試試看突倍。
如何破壞一個(gè)單例
反射攻擊
直接上代碼:
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);
}
}
執(zhí)行結(jié)果如下:
This is a DoubleCheckLockSingleton 1368884364
This is a DoubleCheckLockSingleton 401625763
false
這種方法非常簡單暴力腔稀,通過反射侵入單例類的私有構(gòu)造方法并強(qiáng)制執(zhí)行,使之產(chǎn)生多個(gè)不同的實(shí)例羽历,這樣單例就被破壞了焊虏。要防御反射攻擊,只能在單例構(gòu)造方法中檢測instance是否為null秕磷,如果已不為null诵闭,就拋出異常。顯然雙重檢查鎖實(shí)現(xiàn)無法做這種檢查澎嚣,靜態(tài)內(nèi)部類實(shí)現(xiàn)則是可以的疏尿。
注意,不能在單例類中添加類初始化的標(biāo)記位或計(jì)數(shù)值(比如boolean flag
易桃、int count
)來防御此類攻擊褥琐,因?yàn)橥ㄟ^反射仍然可以隨意修改它們的值。
序列化攻擊
這種攻擊方式只對(duì)實(shí)現(xiàn)了Serializable接口的單例有效晤郑,但偏偏有些單例就是必須序列化的〉谐剩現(xiàn)在假設(shè)DoubleCheckLockSingleton類已經(jīng)實(shí)現(xiàn)了該接口,上代碼:
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);
}
}
執(zhí)行結(jié)果如下:
This is a DoubleCheckLockSingleton 777874839
This is a DoubleCheckLockSingleton 254413710
false
為什么會(huì)發(fā)生這種事造寝?長話短說磕洪,在ObjectInputStream.readObject()方法執(zhí)行時(shí),其內(nèi)部方法readOrdinaryObject()中有這樣一句話:
obj = desc.isInstantiable() ? desc.newInstance() : null;
其中desc是類描述符诫龙。也就是說褐鸥,如果一個(gè)實(shí)現(xiàn)了Serializable/Externalizable接口的類可以在運(yùn)行時(shí)實(shí)例化,那么就調(diào)用newInstance()方法赐稽,使用其默認(rèn)構(gòu)造方法反射創(chuàng)建新的對(duì)象實(shí)例,自然也就破壞了單例性浑侥。要防御序列化攻擊姊舵,就得將instance聲明為transient,并且在單例中加入以下語句:
private Object readResolve() {
return instance;
}
這是因?yàn)樵谏鲜鰎eadOrdinaryObject()方法中寓落,會(huì)通過衛(wèi)語句desc.hasReadResolveMethod()
檢查類中是否存在名為readResolve()的方法括丁,如果有,就執(zhí)行desc.invokeReadResolve(obj)
調(diào)用該方法伶选。readResolve()會(huì)用自定義的反序列化邏輯覆蓋默認(rèn)實(shí)現(xiàn)史飞,因此強(qiáng)制它返回instance本身,就可以防止產(chǎn)生新的實(shí)例仰税。
枚舉單例的防御機(jī)制
對(duì)反射的防御
我們直接將上述reflectionAttack()方法中的類名改成EnumSingleton并執(zhí)行构资,會(huì)發(fā)現(xiàn)報(bào)如下異常:
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)
這是因?yàn)樗蠮ava枚舉都隱式繼承自Enum抽象類,而Enum抽象類根本沒有無參構(gòu)造方法陨簇,只有如下一個(gè)構(gòu)造方法:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
那么我們就改成獲取這個(gè)有參構(gòu)造方法吐绵,即:
Constructor constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
結(jié)果還是會(huì)拋出異常:
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反射機(jī)制內(nèi)部完全禁止了用反射創(chuàng)建枚舉實(shí)例的可能性己单。
對(duì)序列化的防御
如果將serializationAttack()方法中的攻擊目標(biāo)換成EnumSingleton唉窃,那么我們就會(huì)發(fā)現(xiàn)s1和s2實(shí)際上是同一個(gè)實(shí)例,最終會(huì)打印出true纹笼。這是因?yàn)镺bjectInputStream類中纹份,對(duì)枚舉類型有一個(gè)專門的readEnum()方法來處理,其簡要流程如下:
- 通過類描述符取得枚舉單例的類型EnumSingleton廷痘;
- 取得枚舉單例中的枚舉值的名字(這里是INSTANCE)蔓涧;
- 調(diào)用Enum.valueOf()方法,根據(jù)枚舉類型和枚舉值的名字牍疏,獲得最終的單例蠢笋。
這種處理方法與readResolve()方法大同小異,都是以繞過反射直接獲取單例為目標(biāo)鳞陨。不同的是昨寞,枚舉對(duì)序列化的防御仍然是JDK內(nèi)部實(shí)現(xiàn)的。
綜上所述厦滤,枚舉單例確實(shí)是目前最好的單例實(shí)現(xiàn)了援岩,不僅寫法非常簡單,并且JDK能夠保證其安全性掏导,不需要我們做額外的工作享怀。