不一樣的單例模式

不一樣的單例模式

提起單例模式,大家基本上都不是很陌生蜡坊,它的主要作用是保證在Java的整個(gè)項(xiàng)目中只有一個(gè)對(duì)象的存在惕医,而大家在搜單利模式的時(shí)候基本上也會(huì)搜出各種各樣的寫(xiě)法,比如餓漢式算色,懶漢式抬伺,雙重校驗(yàn)鎖,靜態(tài)代碼塊灾梦,靜態(tài)內(nèi)部類峡钓,枚舉等等的寫(xiě)法,基本上算是老生常談的東西了若河,但是無(wú)論是面試還是日常開(kāi)發(fā)中能岩,單例模式還是挺常用到的,這里介紹一種不一樣寫(xiě)法的單例模式萧福,能幫助大家稍微提升一點(diǎn)逼格~

1. 目標(biāo)

作為單例模式拉鹃,我們有兩個(gè)基本目標(biāo):

  1. 全局唯一
  2. 線程安全

全局唯一和線程安全就不說(shuō)了,基本上所有的單例都能滿足這兩點(diǎn)鲫忍,那么最好我們的單例可以支持懶加載膏燕,同時(shí)在保證線程安全的情況下還能夠高效一些

2. 代碼

首先我們來(lái)看一下代碼:

import java.util.concurrent.atomic.AtomicReference;

/**
 * If there are no bugs, it was created by Chen FengYao on 18-7-16;
 * Otherwise, I don't know who created it either
 */
public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();

    public static Singleton getInstance() {
        for (; ; ) {
            Singleton current = INSTANCE.get();
            if (current != null) {
                return current;
            }
            current = new Singleton();
            if (INSTANCE.compareAndSet(null, current)) {
                return current;
            }
        }
    }

    private Singleton() {
    }
}

我們看到在這種單例的寫(xiě)法中,Singleton的實(shí)例實(shí)在調(diào)用getInstance方法的時(shí)候才被創(chuàng)建出來(lái)的悟民,也就是支持懶加載坝辫,而整段代碼也并沒(méi)有使用任何的線程鎖,而這個(gè)單例得以實(shí)現(xiàn)的核心是AtomicReference這個(gè)類

AtomicReference

根據(jù)API的描述射亏,它是一個(gè)可以保證對(duì)象更新原子性的一個(gè)類近忙,原子意味著多個(gè)線程試圖改變同一個(gè)AtomicReference將不會(huì)引發(fā)線程安全的問(wèn)題

AtomicReference有一個(gè)非常常用的方法:compareAndSet,這個(gè)方法接受兩個(gè)參數(shù)智润,第一個(gè)參數(shù)為期望值及舍,第二個(gè)參數(shù)為你想要設(shè)定的值,這個(gè)方法的含義是窟绷,將AtomicReference中的值更新為第二個(gè)參數(shù)所傳遞的值锯玛,當(dāng)當(dāng)前值為期望值的時(shí)候,如果更新成功钾麸,則返回true更振,否則返回false

單例說(shuō)明

那么接下來(lái)我們來(lái)看看這個(gè)單例的執(zhí)行流程
首先當(dāng)?shù)谝淮握{(diào)用getInstance的時(shí)候炕桨,INSTANCE中并沒(méi)有存儲(chǔ)任何的值,所以current為null肯腕,那么這時(shí)就會(huì)創(chuàng)建current對(duì)象献宫,并嘗試向INSTANCE中更新Singleton的值,只有當(dāng)INSTANCE中的值為null的時(shí)候才可能更新成功实撒,這就保證了在多線程環(huán)境中姊途,只能對(duì)INSTANCE中的值賦值一次,就保證了線程安全

3. 破壞單例

我們?cè)趯?xiě)單例的時(shí)候知态,總是不期望在工程中有多個(gè)實(shí)例的出現(xiàn)捷兰,于是我們將構(gòu)造方法私有化,并且提供了一個(gè)我們可以掌控的入口來(lái)創(chuàng)建出一個(gè)對(duì)象负敏,雖然如此贡茅,我們寫(xiě)的單例模式還是有被破壞的可能,所謂破壞單例其做,就是通過(guò)某種手段在整個(gè)工程中創(chuàng)建出多個(gè)實(shí)例顶考,總體來(lái)說(shuō),破會(huì)單例的方式有兩種:

  1. 通過(guò)反射
  2. 通過(guò)通過(guò)序列化

3.1 通過(guò)反射來(lái)破壞單例

我們知道妖泄,通過(guò)Java的反射技術(shù)驹沿,我們的代碼幾乎處于一種“為所欲為”的狀態(tài),雖然我們?cè)谧约旱膯卫愔袑?gòu)造方法私有化了蹈胡,但是可以通過(guò)反射輕松的創(chuàng)建出對(duì)象渊季,為了讓效果更加的明顯,首先在單例類中增加一個(gè)成員變量:

import java.util.concurrent.atomic.AtomicReference;

/**
 * If there are no bugs, it was created by Chen FengYao on 18-7-16;
 * Otherwise, I don't know who created it either
 */
public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
    
    // 增加的成員變量
    private String name;

    public static Singleton getInstance() {
        for (; ; ) {
            Singleton current = INSTANCE.get();
            if (current != null) {
                return current;
            }
            current = new Singleton();
            if (INSTANCE.compareAndSet(null, current)) {
                return current;
            }
        }
    }

    private Singleton() {
    }

    // setter/getter 方法
    public String getName() {
        return name;
    }

    public Singleton setName(String name) {
        this.name = name;
        return this;
    }
}

然后我們通過(guò)getInstance方法獲得一個(gè)實(shí)例對(duì)象,在通過(guò)反射來(lái)獲取一個(gè)實(shí)例對(duì)象:

import java.lang.reflect.Constructor;

/**
 * If there are no bugs, it was created by Chen FengYao on 18-7-15;
 * Otherwise, I don't know who created it either
 */
public class Main {
    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.getInstance();
        singleton.setName("Tom");
        
        Class<Singleton> singletonClazz = Singleton.class;
        Constructor<Singleton> declaredConstructor = singletonClazz.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Singleton singleton1 = declaredConstructor.newInstance();
        singleton1.setName("Jerry");

        System.out.println(singleton.getName());
        System.out.println(singleton1.getName());

    }
}

運(yùn)行結(jié)果:

image

從運(yùn)行結(jié)果我們可以看出singleton和singleton1是兩個(gè)對(duì)象,就是通過(guò)反射我們調(diào)用了私有化的構(gòu)造方法,如果需要抵御這種攻擊,可以修改構(gòu)造器,通過(guò)獲取方法調(diào)用棧信息來(lái)判斷究竟是我們自己的getInstance方法調(diào)用的還是通過(guò)反射調(diào)用的,如果是通過(guò)反射調(diào)用的,那么我們就拋出一個(gè)運(yùn)行時(shí)異常,在Java中我們可以通過(guò)Throwable類來(lái)獲取方法調(diào)用堆棧信息,首先看看效果,在Singleton的構(gòu)造方法中添加代碼:

private Singleton() {
    Throwable ex = new Throwable();
    StackTraceElement[] stackElements = ex.getStackTrace();
    if (stackElements != null) {
        for (int i = 0; i < stackElements.length; i++) {
            System.out.println(stackElements[i].getClassName());
            System.out.println(stackElements[i].getFileName());
            System.out.println(stackElements[i].getLineNumber());
            System.out.println(stackElements[i].getMethodName());
            System.out.println("-----------------------------------");
        }
    }
}

然后首先看一下通過(guò)正常的getInstance來(lái)獲取對(duì)象時(shí)的日志:


image

可以看到,在第二次循環(huán)中,發(fā)現(xiàn)類名為Singleton這個(gè)類,在看看通過(guò)反射調(diào)用的方法棧:


image

那現(xiàn)在一目了然了,可以看到如果是通過(guò)反射調(diào)用的,在方法的調(diào)用棧中是不會(huì)出現(xiàn)getInstance這個(gè)方法,或者Singleton這個(gè)類的其他信息的,那么我們可以通過(guò)去查詢方法調(diào)用棧來(lái)去判斷是否有人想要通過(guò)反射來(lái)破壞我們的單例,如果有,我們就拋出一個(gè)運(yùn)行時(shí)異常,改造后的代碼如下:

import java.util.concurrent.atomic.AtomicReference;

/**
 * If there are no bugs, it was created by Chen FengYao on 18-7-16;
 * Otherwise, I don't know who created it either
 */
public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();

    // 增加的成員變量
    private String name;

    public static Singleton getInstance() {
        for (; ; ) {
            Singleton current = INSTANCE.get();
            if (current != null) {
                return current;
            }
            current = new Singleton();
            if (INSTANCE.compareAndSet(null, current)) {
                return current;
            }
        }
    }

    private Singleton() {
        IllegalStateException illegalStateException = new IllegalStateException("不能調(diào)用構(gòu)造方法,請(qǐng)使用getInstance()來(lái)獲取實(shí)例");

        StackTraceElement[] stackElements = illegalStateException.getStackTrace();

        if (stackElements != null && !stackElements[1].getClassName().equals(getClass().getName())) {
            throw illegalStateException;
        }
    }

    // setter/getter 方法
    public String getName() {
        return name;
    }

    public Singleton setName(String name) {
        this.name = name;
        return this;
    }
}

主要是改造了它的構(gòu)造方法,在這里選擇的是判斷調(diào)用棧的類名,即調(diào)用構(gòu)造方法的類一定是本類,否則就是通過(guò)非法途徑調(diào)用的,之所以選擇類名,因?yàn)轭惷彩强梢詣?dòng)態(tài)獲取的,這樣代碼一旦寫(xiě)完,后期無(wú)論是想改Singleton這個(gè)類名,還是想改getInstance這個(gè)方法名都是沒(méi)有問(wèn)題的,不需要再改構(gòu)造方法里面的代碼了,運(yùn)行一下看看效果:

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.getInstance();
        singleton.setName("Tom");
        System.out.println(singleton.getName());
        
        System.out.println("+++++++++++++++");
        Class<Singleton> singletonClazz = Singleton.class;
        Constructor<Singleton> declaredConstructor = singletonClazz.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Singleton singleton1 = declaredConstructor.newInstance();
        singleton1.setName("Jerry");

    }
}

運(yùn)行結(jié)果:


image

可以看到使用getInstance方法調(diào)用就不會(huì)有問(wèn)題,而使用反射去調(diào)用構(gòu)造方法就會(huì)拋出異常,讓程序崩潰

3.2 通過(guò)序列化來(lái)破壞單例

如果一個(gè)單例類需要被序列化,那在反序列化的過(guò)程中是很有可能破壞單例的設(shè)計(jì)初衷的,因?yàn)榉葱蛄谢怯锌赡芾@過(guò)構(gòu)造方法的,首先讓Singleton 實(shí)現(xiàn)Serializable接口,然后編寫(xiě)測(cè)試代碼:

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.getInstance();
        singleton.setName("Tom");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
        oos.writeObject(singleton);
        oos.close();

        singleton.setName("Tom0");

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton"));
        Singleton singleton1 = (Singleton) ois.readObject();
        ois.close();

        singleton1.setName("Tom1");

        System.out.println(singleton.getName());
        System.out.println(singleton1.getName());

    }
}
image

可以看到反序列化回來(lái)的并不再是原來(lái)的單例對(duì)象了,如果想要讓反序列話回來(lái)的還是單例對(duì)象,需要在單例類中添加readResolve方法,來(lái)自己實(shí)現(xiàn)反序列化的規(guī)則:

public class Singleton implements Serializable {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();

    // 增加的成員變量
    private String name;

    public static Singleton getInstance() {
        for (; ; ) {
            Singleton current = INSTANCE.get();
            if (current != null) {
                return current;
            }
            current = new Singleton();
            if (INSTANCE.compareAndSet(null, current)) {
                return current;
            }
        }
    }

    private Singleton() {
        IllegalStateException illegalStateException = new IllegalStateException("不能調(diào)用構(gòu)造方法,請(qǐng)使用getInstance()來(lái)獲取實(shí)例");

        StackTraceElement[] stackElements = illegalStateException.getStackTrace();

        if (stackElements != null && !stackElements[1].getClassName().equals(getClass().getName())) {
            throw illegalStateException;
        }
    }

    // 用于反序列化
    private Object readResolve(){
        return getInstance();
    }

    // setter/getter 方法
    public String getName() {
        return name;
    }

    public Singleton setName(String name) {
        this.name = name;
        return this;
    }
}

再次運(yùn)行:

image

他們就是同一個(gè)對(duì)象了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末罚渐,一起剝皮案震驚了整個(gè)濱河市却汉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌搅轿,老刑警劉巖病涨,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異璧坟,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)赎懦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)雀鹃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人励两,你說(shuō)我怎么就攤上這事黎茎。” “怎么了当悔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵傅瞻,是天一觀的道長(zhǎng)踢代。 經(jīng)常有香客問(wèn)我,道長(zhǎng)嗅骄,這世上最難降的妖魔是什么胳挎? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮溺森,結(jié)果婚禮上慕爬,老公的妹妹穿的比我還像新娘。我一直安慰自己屏积,他們只是感情好医窿,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著炊林,像睡著了一般姥卢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上渣聚,一...
    開(kāi)封第一講書(shū)人閱讀 49,842評(píng)論 1 290
  • 那天隔显,我揣著相機(jī)與錄音,去河邊找鬼饵逐。 笑死括眠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的倍权。 我是一名探鬼主播掷豺,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼薄声!你這毒婦竟也來(lái)了当船?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤默辨,失蹤者是張志新(化名)和其女友劉穎德频,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體缩幸,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡壹置,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了表谊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钞护。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖爆办,靈堂內(nèi)的尸體忽然破棺而出难咕,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布余佃,位于F島的核電站暮刃,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏爆土。R本人自食惡果不足惜椭懊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望雾消。 院中可真熱鬧灾搏,春花似錦、人聲如沸立润。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)桑腮。三九已至泉哈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間破讨,已是汗流浹背丛晦。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留提陶,地道東北人烫沙。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像隙笆,于是被迫代替她去往敵國(guó)和親锌蓄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容

  • 前言 本文主要參考 那些年撑柔,我們一起寫(xiě)過(guò)的“單例模式”瘸爽。 何為單例模式? 顧名思義铅忿,單例模式就是保證一個(gè)類僅有一個(gè)...
    tandeneck閱讀 2,490評(píng)論 1 8
  • 單例模式(SingletonPattern)一般被認(rèn)為是最簡(jiǎn)單剪决、最易理解的設(shè)計(jì)模式,也因?yàn)樗暮?jiǎn)潔易懂檀训,是項(xiàng)目中最...
    成熱了閱讀 4,231評(píng)論 4 34
  • 寫(xiě)在前面 寫(xiě)代碼久了不想只是做一個(gè)寫(xiě)寫(xiě)if else的初級(jí)碼農(nóng)柑潦,隨著coding經(jīng)驗(yàn)的積累以及對(duì)這份職業(yè)的更高期望...
    Alexyz123閱讀 513評(píng)論 0 2
  • 今天晚上從糖球會(huì)回來(lái)在網(wǎng)上無(wú)意間查閱一些資料,看到了嗨啤字眼肢扯,然后就順著搜索看了看2014年的時(shí)候大家一起玩即興脫...
    祥祥布魯斯閱讀 166評(píng)論 0 1
  • 辭職總算可以了妒茬,突然有一種好放松的,憋了好久才勇敢的跨出這一步蔚晨,心驚膽蔵這么久,比發(fā)獎(jiǎng)金還高興。 人生不如意的事莫...
    踩花大俠閱讀 196評(píng)論 2 0