為什么說枚舉是最好的Java單例實(shí)現(xiàn)方法?

很久沒有寫過接地氣的東西了载佳,今天隨便寫一個(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能夠保證其安全性掏导,不需要我們做額外的工作享怀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市趟咆,隨后出現(xiàn)的幾起案子添瓷,更是在濱河造成了極大的恐慌,老刑警劉巖值纱,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鳞贷,死亡現(xiàn)場離奇詭異,居然都是意外死亡虐唠,警方通過查閱死者的電腦和手機(jī)搀愧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疆偿,“玉大人咱筛,你說我怎么就攤上這事「斯剩” “怎么了迅箩?”我有些...
    開封第一講書人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長处铛。 經(jīng)常有香客問我沙热,道長叉钥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任篙贸,我火速辦了婚禮投队,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘爵川。我一直安慰自己敷鸦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開白布寝贡。 她就那樣靜靜地躺著扒披,像睡著了一般。 火紅的嫁衣襯著肌膚如雪圃泡。 梳的紋絲不亂的頭發(fā)上碟案,一...
    開封第一講書人閱讀 52,184評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音颇蜡,去河邊找鬼价说。 笑死,一個(gè)胖子當(dāng)著我的面吹牛风秤,可吹牛的內(nèi)容都是我干的鳖目。 我是一名探鬼主播,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼缤弦,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼领迈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起碍沐,我...
    開封第一講書人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤狸捅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后累提,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尘喝,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年刻恭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扯夭。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鳍贾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出交洗,到底是詐尸還是另有隱情骑科,我是刑警寧澤,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布构拳,位于F島的核電站咆爽,受9級(jí)特大地震影響梁棠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜斗埂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一符糊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧呛凶,春花似錦男娄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至崭捍,卻和暖如春尸折,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背殷蛇。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來泰國打工实夹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人晾咪。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓收擦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谍倦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子塞赂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359