圣誕節(jié)科阎,讓我們聊聊單例模式

圣誕節(jié)到了述吸,是時(shí)候?qū)卫幸粋€(gè)新的認(rèn)識(shí)了,不然一個(gè)就會(huì)變成兩個(gè)锣笨、四個(gè)...很多個(gè)...嗯蝌矛,我說(shuō)的是圣誕老人...

很久之前看到一篇講單例的文章,看完才知道看似簡(jiǎn)單的單例模式错英,其實(shí)有很大的考究入撒,最近又看到了幾篇類似的文章,發(fā)現(xiàn)單例其實(shí)很復(fù)雜椭岩。費(fèi)了很大力氣茅逮,理順了思路,頓時(shí)又覺(jué)得單例模式可以不用那么復(fù)雜了判哥。

首先献雅,我們得問(wèn)自己一個(gè)問(wèn)題:為什么要使用單例?

為什么要使用單例

單例塌计,顧名思義挺身,就是讓一個(gè)類只存在一個(gè)實(shí)例對(duì)象,那么什么時(shí)候我們會(huì)需要單例呢锌仅?最常見(jiàn)的有以下兩種情形:

  • 無(wú)狀態(tài)的工具類:比如日志工具類章钾,不管是在哪里使用,我們需要的只是它幫我們記錄日志信息热芹,除此之外,并不需要在它的實(shí)例對(duì)象上存儲(chǔ)任何狀態(tài)窍箍,這時(shí)候我們就只需要一個(gè)實(shí)例對(duì)象即可。
  • 全局信息類:比如我們?cè)谝粋€(gè)類上記錄網(wǎng)站的訪問(wèn)次數(shù)椰棘,我們不希望有的訪問(wèn)被記錄在對(duì)象A上纺棺,有的卻記錄在對(duì)象B上,這時(shí)候我們就讓這個(gè)類成為單例祷蝌。

單例起到的好處主要有兩點(diǎn):

  • 節(jié)省內(nèi)存
  • 方便管理

值得注意的是帆卓,單例往往都可以通過(guò)static來(lái)實(shí)現(xiàn)巨朦,把一個(gè)實(shí)例方法變成靜態(tài)方法,或者把一個(gè)實(shí)例變量變成靜態(tài)變量剑令,都可以起到單例的效果。在我看來(lái)吁津,這只是面向?qū)ο蠛兔嫦蜻^(guò)程的區(qū)別。

一個(gè)完美的懶漢模式

了解完為什么要使用單例梭依,接下來(lái)讓我們來(lái)實(shí)現(xiàn)一個(gè)完美的單例模式典尾。
實(shí)現(xiàn)單例模式,你只需要注意以下幾點(diǎn):

  1. 構(gòu)造函數(shù)私有化河闰,防止別的開(kāi)發(fā)人員調(diào)用而創(chuàng)建出多個(gè)實(shí)例
  2. 在類的內(nèi)部創(chuàng)建實(shí)例勃教,創(chuàng)建時(shí)要注意多線程并發(fā)訪問(wèn)可能導(dǎo)致的new出多個(gè)實(shí)例的問(wèn)題
  3. 提供獲取唯一實(shí)例的方法

基于以上三點(diǎn)故源,我們實(shí)現(xiàn)了下面這個(gè)“懶漢”單例模式(本文的所有代碼汞贸,可到Github上下載):

public class PerfectLazyManSingleton {
    private volatile static PerfectLazyManSingleton instance = null;

    private PerfectLazyManSingleton() {
    }

    public static PerfectLazyManSingleton getInstance() {
        if(instance == null) {
            synchronized (PerfectLazyManSingleton.class) {
                if(instance == null) {
                    instance = new PerfectLazyManSingleton();
                }
            }
        }
        return instance;
    }
}

這個(gè)單例在實(shí)際使用中已經(jīng)是完美的了:

  • 使用私有構(gòu)造函數(shù)防止new出多個(gè)實(shí)例
  • 使用Double-Check + synchronized同步鎖矢腻,解決多線程并發(fā)訪問(wèn)可能導(dǎo)致的在內(nèi)部調(diào)用多次new的問(wèn)題
  • 使用volatile關(guān)鍵字,解決由于指令重排而可能出現(xiàn)的在內(nèi)部調(diào)用多次new的問(wèn)題

至于很多文章里說(shuō)的利用類加載器奶是、利用反射等創(chuàng)建多個(gè)實(shí)例的問(wèn)題,我們只需要知道有這個(gè)可能性就好聂沙,因?yàn)檫@些都不是正常創(chuàng)建對(duì)象的方式,我們使用單例模式是為了防止其他開(kāi)發(fā)人員不小心new出多個(gè)實(shí)例沮趣,而如果開(kāi)發(fā)人員都動(dòng)用了反射和ClassLoader這些重型武器了房铭,那我想這絕對(duì)不是“不小心”了温眉。

與其浪費(fèi)心思、犧牲代碼可讀性豪嗽、犧牲性能豌骏,去獲取“絕對(duì)意義”上的單例,還不如在類上面加上行注釋——“This is a single-instance class. Do not try to create another instance”计贰,來(lái)提示那些看到私有構(gòu)造函數(shù)還不知道這是個(gè)單例的新手們蒂窒,不要嘗試創(chuàng)建新的實(shí)例了洒琢!

如果真想實(shí)現(xiàn)“絕對(duì)意義”上的單例,那就使用枚舉吧象迎。

單例工廠

消除重復(fù)是程序員的天性呛踊,如果我們每次需要單例對(duì)象時(shí),都按照上面的模式把類設(shè)計(jì)成單例谭网,那顯然是不可接受的愉择。這時(shí)候我們就可以設(shè)計(jì)一個(gè)單例工廠织中,這個(gè)單例工廠就像民政局一樣衷戈,我給他一個(gè)身份證號(hào)碼脱惰,他給我返回唯一一個(gè)對(duì)應(yīng)的人。

public class SingletonRegistry {
    public static SingletonRegistry REGISTRY = new SingletonRegistry();
    private static HashMap map = new HashMap();
    private static Logger logger = LoggerFactory.getLogger(SingletonRegistry.class);

    private SingletonRegistry() {
    }

    public static synchronized Object getInstance(String classname) {
        Object singleton = map.get(classname);
        if (singleton != null) {
            return singleton;
        }
        try {
            singleton = Class.forName(classname).newInstance();
            logger.info("created singleton: " + singleton);
        } catch (ClassNotFoundException cnf) {
            logger.warn("Couldn't find class " + classname);
        } catch (InstantiationException ie) {
            logger.warn("Couldn't instantiate an object of type " +
                    classname);
        } catch (IllegalAccessException ia) {
            logger.warn("Couldn't access class " + classname);
        }
        map.put(classname, singleton);
        return singleton;
    }
}

關(guān)于這個(gè)SingletonRegistry采盒,有以下幾點(diǎn)需要注意的:

  • 這個(gè)SingletonRegistry本身也是單例磅氨,使用的是“餓漢”版的單例模式
  • 由于getInstance方法要返回的實(shí)例不再是類的成員變量嫡纠,因此不再能夠使用volatile來(lái)獲得線程之間的可見(jiàn)性,因此要將整個(gè)getInstance方法加上同步鎖

這個(gè)單例工廠的用法非常簡(jiǎn)單:

public class Singleton {
   private Singleton() {
   }
   public static Singleton getInstance() {
      return (Singleton)SingletonRegistry.REGISTRY.getInstance(classname);
   }
}

餓漢版單例模式

餓漢版的單例模式非常簡(jiǎn)單叉橱,上面的SingletonRegistry其實(shí)就是“餓漢”版的單例模式窃祝,一個(gè)完美的餓漢單例模式代碼如下:

public class SingletonHungryMan {
    public final static SingletonHungryMan INSTANCE = new SingletonHungryMan();
    private SingletonHungryMan() {
        // Exists only to defeat instantiation.
    }
    public void sayHello() {
        System.out.println("hello");
    }

}

為什么這里就不用擔(dān)心多線程并發(fā)導(dǎo)致的new了多個(gè)示例呢踱侣?
關(guān)鍵在于這是static靜態(tài)變量抡句,而靜態(tài)變量歸屬于類,會(huì)在類加載的過(guò)程中被初始化逞壁,而Java類加載的過(guò)程默認(rèn)是線程安全的究抓,除非自定義的類加載器覆寫(xiě)了loadClass函數(shù)袭灯。
下面就是ClassLoader的loadClass方法稽荧,這個(gè)方法很好的展示了什么是雙親委派模型:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

當(dāng)然了,餓漢版的單例模式如果受到非常規(guī)的攻擊擅腰,還是會(huì)生出二胎出來(lái)的翁潘,比如利用反射把私有的構(gòu)造器設(shè)為Accessible,抑或是使用自定義的類加載器進(jìn)行加載渗勘,產(chǎn)生新的實(shí)例俩莽。

對(duì)于任意一個(gè)類扮超,都需要由加載它的類加載器和這個(gè)類本身一同確立其在Java虛擬機(jī)中的唯一性 —— 《深入理解Java虛擬機(jī)》 第7章 虛擬機(jī)類加載機(jī)制

我分別使用了反射和類加載器,對(duì)上面的SingletonHungryMan進(jìn)行了攻擊璧疗,代碼如下:

public class SingletonHungryManTest {
    private SingletonHungryMan sone = null;
    private Object stwo = null;
    private Object sthree = null;
    private static Logger logger = LoggerFactory.getLogger(SingletonHungryManTest.class);

    @Before
    public void setUp() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        sone = SingletonHungryMan.INSTANCE;
        stwo = createAnotherInstanceUsingRelection();
        sthree = createAnotherInstanceUsingAnotherClassLoader();
    }

    private Object createAnotherInstanceUsingRelection() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Class<SingletonHungryMan> singletonHungryManClass = SingletonHungryMan.class;
        Constructor<?> declaredConstructor = singletonHungryManClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        return declaredConstructor.newInstance();
    }

    private Object createAnotherInstanceUsingAnotherClassLoader() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchFieldException {
        // use custom class loader to load class
        ClassLoader myLoader = getMyLoader();
        Class<?> myClass = myLoader.loadClass("com.sexycode.codepractice.singleton.SingletonHungryMan");
        // use reflection to get field
        Field field = myClass.getField("INSTANCE");
        // return the field's value
        return field.get(null);
    }

    private ClassLoader getMyLoader() throws ClassNotFoundException {
        return new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
    }

    @Test
    public void testUnique() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        logger.info("checking singletons for equality");
        sone.sayHello();
        invokeMethod(stwo, "sayHello");
        invokeMethod(sthree, "sayHello");
        Assert.assertNotEquals(true, sone == stwo);
        Assert.assertNotEquals(true, sone == sthree);
    }

    private void invokeMethod(Object obj, String method) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Method sayHello = obj.getClass().getMethod(method);
        sayHello.invoke(obj);
    }
}

可序列化對(duì)象的單例

可序列化對(duì)象,在進(jìn)行序列化之后啦膜,可以進(jìn)行多次的反序列化淌喻,這時(shí)候如果要維持單例,就要實(shí)現(xiàn)readResolve方法:

public class SingletonSerializable implements java.io.Serializable {
    public static SingletonSerializable INSTANCE = new SingletonSerializable();

    private SingletonSerializable() {
        // Exists only to thwart instantiation.
    }

    private Object readResolve() {
        return INSTANCE;
    }

}

小結(jié)

實(shí)現(xiàn)單例模式八拱,其實(shí)沒(méi)有那么復(fù)雜肌稻,我們要考慮的只是如何防止其他開(kāi)發(fā)人員在常規(guī)操作下創(chuàng)建多個(gè)實(shí)例匕荸,至于那些非常規(guī)的手段榛搔,并不值得犧牲代碼可讀性和性能去進(jìn)行防御东揣。

最后再拋出一個(gè)問(wèn)題腹泌,Spring的@Scope("singleton")是怎么實(shí)現(xiàn)單例的呢?

最最重要的是芥吟,圣誕節(jié)來(lái)了专甩,你知道怎么實(shí)現(xiàn)單例配深、防止多例了么?

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市缸托,隨后出現(xiàn)的幾起案子俐镐,更是在濱河造成了極大的恐慌,老刑警劉巖叼风,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棍苹,死亡現(xiàn)場(chǎng)離奇詭異枢里,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)彬碱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)奥洼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)溉卓,“玉大人,你說(shuō)我怎么就攤上這事伏尼∥疚玻” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵辨图,是天一觀的道長(zhǎng)故河。 經(jīng)常有香客問(wèn)我吆豹,道長(zhǎng),這世上最難降的妖魔是什么凑阶? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任宙橱,我火速辦了婚禮蘸拔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘呕乎。我一直安慰自己陨晶,他們只是感情好先誉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布褐耳。 她就那樣靜靜地躺著,像睡著了一般铃芦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仁烹,一...
    開(kāi)封第一講書(shū)人閱讀 51,718評(píng)論 1 305
  • 那天卓缰,我揣著相機(jī)與錄音征唬,去河邊找鬼。 笑死总寒,一個(gè)胖子當(dāng)著我的面吹牛摄闸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播媳禁,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼竣稽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼霍弹!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起岛宦,我...
    開(kāi)封第一講書(shū)人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤砾肺,失蹤者是張志新(化名)和其女友劉穎防嗡,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體裙盾,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡番官,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年徘熔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片生音。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖慕匠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蓉媳,我是刑警寧澤锅铅,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布盐须,位于F島的核電站,受9級(jí)特大地震影響阶冈,放射性物質(zhì)發(fā)生泄漏塑径。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望誉简。 院中可真熱鬧描融,春花似錦、人聲如沸骏庸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)一姿。三九已至,卻和暖如春艾栋,著一層夾襖步出監(jiān)牢的瞬間蛉顽,已是汗流浹背携冤。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扣猫,地道東北人翘地。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓子眶,卻偏偏與公主長(zhǎng)得像臭杰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子渴杆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

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

  • 單例模式(SingletonPattern)一般被認(rèn)為是最簡(jiǎn)單囊拜、最易理解的設(shè)計(jì)模式比搭,也因?yàn)樗暮?jiǎn)潔易懂,是項(xiàng)目中最...
    成熱了閱讀 4,254評(píng)論 4 34
  • 1 場(chǎng)景問(wèn)題# 1.1 讀取配置文件的內(nèi)容## 考慮這樣一個(gè)應(yīng)用蜜托,讀取配置文件的內(nèi)容。 很多應(yīng)用項(xiàng)目幔托,都有與應(yīng)用相...
    七寸知架構(gòu)閱讀 6,773評(píng)論 12 68
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理重挑,服務(wù)發(fā)現(xiàn)棠涮,斷路器,智...
    卡卡羅2017閱讀 134,665評(píng)論 18 139
  • 1 單例模式的動(dòng)機(jī) 對(duì)于一個(gè)軟件系統(tǒng)的某些類而言隅津,我們無(wú)須創(chuàng)建多個(gè)實(shí)例。舉個(gè)大家都熟知的例子——Windows任務(wù)...
    justCode_閱讀 1,433評(píng)論 2 9
  • 就是不為什么的為什么 每年過(guò)年時(shí)的一大盛事就是同學(xué)朋友之間的聚會(huì)结窘。有的三五好友在一起吃飯聊天,有的一群人在飯店餐館...
    陶之夭夭1閱讀 435評(píng)論 5 5