當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]前言單例模式無論在我們面試,還是日常工作中,都會(huì)面對(duì)的問題。但很多單例模式的細(xì)節(jié),值得我們深入探索一下。這篇文章透過單例模式,串聯(lián)了多方面基礎(chǔ)知識(shí),非常值得一讀。1什么是單例模式?單例模式是一種非常常用的軟件設(shè)計(jì)模式,它定義是單例對(duì)象的類只能允許一個(gè)實(shí)例存在。該類負(fù)責(zé)創(chuàng)建自己的對(duì)...

前言

單例模式無論在我們面試,還是日常工作中,都會(huì)面對(duì)的問題。但很多單例模式的細(xì)節(jié),值得我們深入探索一下。

這篇文章透過單例模式,串聯(lián)了多方面基礎(chǔ)知識(shí),非常值得一讀。

單例模式,真不簡(jiǎn)單

1 什么是單例模式?

單例模式是一種非常常用的軟件設(shè)計(jì)模式,它定義是單例對(duì)象的類只能允許一個(gè)實(shí)例存在。

該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有一個(gè)對(duì)象被創(chuàng)建。一般常用在工具類的實(shí)現(xiàn)或創(chuàng)建對(duì)象需要消耗資源的業(yè)務(wù)場(chǎng)景。

單例模式的特點(diǎn):

  • 類構(gòu)造器私有
  • 持有自己類的引用
  • 對(duì)外提供獲取實(shí)例的靜態(tài)方法
我們先用一個(gè)簡(jiǎn)單示例了解一下單例模式的用法。

public?class?SimpleSingleton?{
????//持有自己類的引用
????private?static?final?SimpleSingleton?INSTANCE?=?new?SimpleSingleton();

????//私有的構(gòu)造方法
????private?SimpleSingleton()?{
????}
????//對(duì)外提供獲取實(shí)例的靜態(tài)方法
????public?static?SimpleSingleton?getInstance()?{
????????return?INSTANCE;
????}
????
????public?static?void?main(String[]?args)?{
????????System.out.println(SimpleSingleton.getInstance().hashCode());
????????System.out.println(SimpleSingleton.getInstance().hashCode());
????}
}
打印結(jié)果:

1639705018
1639705018
我們看到兩次獲取SimpleSingleton實(shí)例的hashCode是一樣的,說明兩次調(diào)用獲取到的是同一個(gè)對(duì)象。

可能很多朋友平時(shí)工作當(dāng)中都是這么用的,但我要說這段代碼是有問題的,你會(huì)相信嗎?

不信,我們一起往下看。

2 餓漢和懶漢模式

在介紹單例模式的時(shí)候,必須要先介紹它的兩種非常著名的實(shí)現(xiàn)方式:餓漢模式懶漢模式。

2.1 餓漢模式

實(shí)例在初始化的時(shí)候就已經(jīng)建好了,不管你有沒有用到,先建好了再說。具體代碼如下:

public?class?SimpleSingleton?{
????//持有自己類的引用
????private?static?final?SimpleSingleton?INSTANCE?=?new?SimpleSingleton();

????//私有的構(gòu)造方法
????private?SimpleSingleton()?{
????}
????//對(duì)外提供獲取實(shí)例的靜態(tài)方法
????public?static?SimpleSingleton?getInstance()?{
????????return?INSTANCE;
????}
}
餓漢模式,其實(shí)還有一個(gè)變種:

public?class?SimpleSingleton?{
????//持有自己類的引用
????private?static?final?SimpleSingleton?INSTANCE;
????static?{
???????INSTANCE?=?new?SimpleSingleton();
????}

????//私有的構(gòu)造方法
????private?SimpleSingleton()?{
????}
????//對(duì)外提供獲取實(shí)例的靜態(tài)方法
????public?static?SimpleSingleton?getInstance()?{
????????return?INSTANCE;
????}
}
使用靜態(tài)代碼塊的方式實(shí)例化INSTANCE對(duì)象。

使用餓漢模式的好處是:沒有線程安全的問題,但帶來的壞處也很明顯。

private?static?final?SimpleSingleton?INSTANCE?=?new?SimpleSingleton();
一開始就實(shí)例化對(duì)象了,如果實(shí)例化過程非常耗時(shí),并且最后這個(gè)對(duì)象沒有被使用,不是白白造成資源浪費(fèi)嗎?

還真是啊。

這個(gè)時(shí)候你也許會(huì)想到,不用提前實(shí)例化對(duì)象,在真正使用的時(shí)候再實(shí)例化不就可以了?

這就是我接下來要介紹的:懶漢模式。

2.2 懶漢模式

顧名思義就是實(shí)例在用到的時(shí)候才去創(chuàng)建,“比較懶”,用的時(shí)候才去檢查有沒有實(shí)例,如果有則返回,沒有則新建。具體代碼如下:

public?class?SimpleSingleton2?{

????private?static?SimpleSingleton2?INSTANCE;

????private?SimpleSingleton2()?{
????}

????public?static?SimpleSingleton2?getInstance()?{
????????if?(INSTANCE?==?null)?{
????????????INSTANCE?=?new?SimpleSingleton2();
????????}
????????return?INSTANCE;
????}
}
示例中的INSTANCE對(duì)象一開始是空的,在調(diào)用getInstance方法才會(huì)真正實(shí)例化。

嗯,不錯(cuò)不錯(cuò)。但這段代碼還是有問題。

2.3 synchronized關(guān)鍵字

上面的代碼有什么問題?

答:假如有多個(gè)線程中都調(diào)用了getInstance方法,那么都走到 if (INSTANCE == null) 判斷時(shí),可能同時(shí)成立,因?yàn)镮NSTANCE初始化時(shí)默認(rèn)值是null。這樣會(huì)導(dǎo)致多個(gè)線程中同時(shí)創(chuàng)建INSTANCE對(duì)象,即INSTANCE對(duì)象被創(chuàng)建了多次,違背了只創(chuàng)建一個(gè)INSTANCE對(duì)象的初衷。

那么,要如何改進(jìn)呢?

答:最簡(jiǎn)單的辦法就是使用synchronized關(guān)鍵字。

改進(jìn)后的代碼如下:

public?class?SimpleSingleton3?{
????private?static?SimpleSingleton3?INSTANCE;

????private?SimpleSingleton3()?{
????}

????public?synchronized?static?SimpleSingleton3?getInstance()?{
????????if?(INSTANCE?==?null)?{
????????????INSTANCE?=?new?SimpleSingleton3();
????????}
????????return?INSTANCE;
????}
????public?static?void?main(String[]?args)?{
????????System.out.println(SimpleSingleton3.getInstance().hashCode());
????????System.out.println(SimpleSingleton3.getInstance().hashCode());
????}
}
在getInstance方法上加synchronized關(guān)鍵字,保證在并發(fā)的情況下,只有一個(gè)線程能創(chuàng)建INSTANCE對(duì)象的實(shí)例。

這樣總可以了吧?

答:不好意思,還是有問題。

有什么問題?

答:使用synchronized關(guān)鍵字會(huì)消耗getInstance方法的性能,我們應(yīng)該判斷當(dāng)INSTANCE為空時(shí)才加鎖,如果不為空不應(yīng)該加鎖,需要直接返回。

這就需要使用下面要說的雙重檢查鎖了。

2.4 餓漢和懶漢模式的區(qū)別

but,在介紹雙重檢查鎖之前,先插播一個(gè)朋友們可能比較關(guān)心的話題:餓漢模式 和 懶漢模式 各有什么優(yōu)缺點(diǎn)?

  • 餓漢模式:優(yōu)點(diǎn)是沒有線程安全的問題,缺點(diǎn)是浪費(fèi)內(nèi)存空間。
  • 懶漢模式:優(yōu)點(diǎn)是沒有內(nèi)存空間浪費(fèi)的問題,缺點(diǎn)是如果控制不好,實(shí)際上不是單例的。
好了,下面可以安心的看看雙重檢查鎖,是如何保證性能的,同時(shí)又保證單例的。

3 雙重檢查鎖

雙重檢查鎖顧名思義會(huì)檢查兩次:在加鎖之前檢查一次是否為空,加鎖之后再檢查一次是否為空。

那么,它是如何實(shí)現(xiàn)單例的呢?

3.1 如何實(shí)現(xiàn)單例?

具體代碼如下:

public?class?SimpleSingleton4?{

????private?static?SimpleSingleton4?INSTANCE;

????private?SimpleSingleton4()?{
????}

????public?static?SimpleSingleton4?getInstance()?{
????????if?(INSTANCE?==?null)?{
????????????synchronized?(SimpleSingleton4.class)?{
????????????????if?(INSTANCE?==?null)?{
????????????????????INSTANCE?=?new?SimpleSingleton4();
????????????????}
????????????}
????????}
????????return?INSTANCE;
????}
}
在加鎖之前判斷是否為空,可以確保INSTANCE不為空的情況下,不用加鎖,可以直接返回。

為什么在加鎖之后,還需要判斷INSTANCE是否為空呢?

答:是為了防止在多線程并發(fā)的情況下,只會(huì)實(shí)例化一個(gè)對(duì)象。

比如:線程a和線程b同時(shí)調(diào)用getInstance方法,假如同時(shí)判斷INSTANCE都為空,這時(shí)會(huì)同時(shí)進(jìn)行搶鎖。

假如線程a先搶到鎖,開始執(zhí)行synchronized關(guān)鍵字包含的代碼,此時(shí)線程b處于等待狀態(tài)。

線程a創(chuàng)建完新實(shí)例了,釋放鎖了,此時(shí)線程b拿到鎖,進(jìn)入synchronized關(guān)鍵字包含的代碼,如果沒有再判斷一次INSTANCE是否為空,則可能會(huì)重復(fù)創(chuàng)建實(shí)例。

所以需要在synchronized前后兩次判斷。

不要以為這樣就完了,還有問題呢?

3.2 volatile關(guān)鍵字

上面的代碼還有啥問題?

public?static?SimpleSingleton4?getInstance()?{
??????if?(INSTANCE?==?null)?{//1
??????????synchronized?(SimpleSingleton4.class)?{//2
??????????????if?(INSTANCE?==?null)?{//3
??????????????????INSTANCE?=?new?SimpleSingleton4();//4
??????????????}
??????????}
??????}
??????return?INSTANCE;//5
??}
getInstance方法的這段代碼,我是按1、2、3、4、5這種順序?qū)懙?,希望也按這個(gè)順序執(zhí)行。

但是java虛擬機(jī)實(shí)際上會(huì)做一些優(yōu)化,對(duì)一些代碼指令進(jìn)行重排。重排之后的順序可能就變成了:1、3、2、4、5,這樣在多線程的情況下同樣會(huì)創(chuàng)建多次實(shí)例。重排之后的代碼可能如下:

public?static?SimpleSingleton4?getInstance()?{
????if?(INSTANCE?==?null)?{//1
???????if?(INSTANCE?==?null)?{//3
???????????synchronized?(SimpleSingleton4.class)?{//2
????????????????INSTANCE?=?new?SimpleSingleton4();//4
????????????}
????????}
????}
????return?INSTANCE;//5
}
原來如此,那有什么辦法可以解決呢?

答:可以在定義INSTANCE是加上volatile關(guān)鍵字。具體代碼如下:

public?class?SimpleSingleton7?{

????private?volatile?static?SimpleSingleton7?INSTANCE;

????private?SimpleSingleton7()?{
????}

????public?static?SimpleSingleton7?getInstance()?{
????????if?(INSTANCE?==?null)?{
????????????synchronized?(SimpleSingleton7.class)?{
????????????????if?(INSTANCE?==?null)?{
????????????????????INSTANCE?=?new?SimpleSingleton7();
????????????????}
????????????}
????????}
????????return?INSTANCE;
????}
}
volatile關(guān)鍵字可以保證多個(gè)線程的可見性,但是不能保證原子性。同時(shí)它也能禁止指令重排。

雙重檢查鎖的機(jī)制既保證了線程安全,又比直接上鎖提高了執(zhí)行效率,還節(jié)省了內(nèi)存空間。

除了上面的單例模式之外,還有沒有其他的單例模式?

4 靜態(tài)內(nèi)部類

靜態(tài)內(nèi)部類顧名思義是通過靜態(tài)的內(nèi)部類來實(shí)現(xiàn)單例模式的。

那么,它是如何實(shí)現(xiàn)單例的呢?

4.1 如何實(shí)現(xiàn)單例模式?

具體代碼如下:

public?class?SimpleSingleton5?{

????private?SimpleSingleton5()?{
????}

????public?static?SimpleSingleton5?getInstance()?{
????????return?Inner.INSTANCE;
????}

????private?static?class?Inner?{
????????private?static?final?SimpleSingleton5?INSTANCE?=?new?SimpleSingleton5();
????}
}
我們看到在SimpleSingleton5類中定義了一個(gè)靜態(tài)的內(nèi)部類Inner。在SimpleSingleton5類的getInstance方法中,返回的是內(nèi)部類Inner的實(shí)例INSTANCE對(duì)象。

只有在程序第一次調(diào)用getInstance方法時(shí),虛擬機(jī)才加載Inner并實(shí)例化INSTANCE對(duì)象。

java內(nèi)部機(jī)制保證了,只有一個(gè)線程可以獲得對(duì)象鎖,其他的線程必須等待,保證對(duì)象的唯一性。

4.2 反射漏洞

上面的代碼看似完美,但還是有漏洞。如果其他人使用反射,依然能夠通過類的無參構(gòu)造方式創(chuàng)建對(duì)象。例如:

Class?simpleSingleton5Class?=?SimpleSingleton5.class;
try?{
????SimpleSingleton5?newInstance?=?simpleSingleton5Class.newInstance();
????System.out.println(newInstance?==?SimpleSingleton5.getInstance());
}?catch?(InstantiationException?e)?{
????e.printStackTrace();
}?catch?(IllegalAccessException?e)?{
????e.printStackTrace();
}
上面代碼打印結(jié)果是false。

由此看出,通過反射創(chuàng)建的對(duì)象,跟通過getInstance方法獲取的對(duì)象,并非同一個(gè)對(duì)象,也就是說,這個(gè)漏洞會(huì)導(dǎo)致SimpleSingleton5非單例。

那么,要如何防止這個(gè)漏洞呢?

答:這就需要在無參構(gòu)造方式中判斷,如果非空,則拋出異常了。

改造后的代碼如下:

public?class?SimpleSingleton5?{

????private?SimpleSingleton5()?{
????????if(Inner.INSTANCE?!=?null)?{
???????????throw?new?RuntimeException("不能支持重復(fù)實(shí)例化");
???????}
????}

????public?static?SimpleSingleton5?getInstance()?{
????????return?Inner.INSTANCE;
????}

????private?static?class?Inner?{
????????private?static?final?SimpleSingleton5?INSTANCE?=?new?SimpleSingleton5();
????????}
????}

}
如果此時(shí),你認(rèn)為這種靜態(tài)內(nèi)部類,實(shí)現(xiàn)單例模式的方法,已經(jīng)完美了。

那么,我要告訴你的是,你錯(cuò)了,還有漏洞。。。

4.3 反序列化漏洞

眾所周知,java中的類通過實(shí)現(xiàn)Serializable接口,可以實(shí)現(xiàn)序列化。

我們可以把類的對(duì)象先保存到內(nèi)存,或者某個(gè)文件當(dāng)中。后面在某個(gè)時(shí)刻,再恢復(fù)成原始對(duì)象。

具體代碼如下:

public?class?SimpleSingleton5?implements?Serializable?{

????private?SimpleSingleton5()?{
????????if?(Inner.INSTANCE?!=?null)?{
????????????throw?new?RuntimeException("不能支持重復(fù)實(shí)例化");
????????}
????}

????public?static?SimpleSingleton5?getInstance()?{
????????return?Inner.INSTANCE;
????}

????private?static?class?Inner?{
????????private?static?final?SimpleSingleton5?INSTANCE?=?new?SimpleSingleton5();
????}

????private?static?void?writeFile()?{
????????FileOutputStream?fos?=?null;
????????ObjectOutputStream?oos?=?null;
????????try?{
????????????SimpleSingleton5?simpleSingleton5?=?SimpleSingleton5.getInstance();
????????????fos?=?new?FileOutputStream(new?File("test.txt"));
????????????oos?=?new?ObjectOutputStream(fos);
????????????oos.writeObject(simpleSingleton5);
????????????System.out.println(simpleSingleton5.hashCode());
????????}?catch?(FileNotFoundException?e)?{
????????????e.printStackTrace();
????????}?catch?(IOException?e)?{
????????????e.printStackTrace();
????????}?finally?{
????????????if?(oos?!=?null)?{
????????????????try?{
????????????????????oos.close();
????????????????}?catch?(IOException?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????}
????????????if?(fos?!=?null)?{
????????????????try?{
????????????????????fos.close();
????????????????}?catch?(IOException?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????}

????????}
????}

????private?static?void?readFile()?{
????????FileInputStream?fis?=?null;
????????ObjectInputStream?ois?=?null;
????????try?{
????????????fis?=?new?FileInputStream(new?File("test.txt"));
????????????ois?=?new?ObjectInputStream(fis);
????????????SimpleSingleton5?myObject?=?(SimpleSingleton5)?ois.readObject();

????????????System.out.println(myObject.hashCode());
????????}?catch?(FileNotFoundException?e)?{
????????????e.printStackTrace();
????????}?catch?(IOException?e)?{
????????????e.printStackTrace();
????????}?catch?(ClassNotFoundException?e)?{
????????????e.printStackTrace();
????????}?finally?{
????????????if?(ois?!=?null)?{
????????????????try?{
????????????????????ois.close();
????????????????}?catch?(IOException?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????}
????????????if?(fis?!=?null)?{
????????????????try?{
????????????????????fis.close();
????????????????}?catch?(IOException?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????}
????????}
????}

????public?static?void?main(String[]?args)?{
????????writeFile();
????????readFile();
????}
}
運(yùn)行之后,發(fā)現(xiàn)序列化和反序列化后對(duì)象的hashCode不一樣:

189568618
793589513
說明,反序列化時(shí)創(chuàng)建了一個(gè)新對(duì)象,打破了單例模式對(duì)象唯一性的要求。

那么,如何解決這個(gè)問題呢?

答:重新readResolve方法。

在上面的實(shí)例中,增加如下代碼:

private?Object?readResolve()?throws?ObjectStreamException?{
????return?Inner.INSTANCE;
}
運(yùn)行結(jié)果如下:

290658609
290658609
我們看到序列化和反序列化實(shí)例對(duì)象的hashCode相同了。

做法很簡(jiǎn)單,只需要在readResolve方法中,每次都返回唯一的Inner.INSTANCE對(duì)象即可。

程序在反序列化獲取對(duì)象時(shí),會(huì)去尋找readResolve()方法。

  • 如果該方法不存在,則直接返回新對(duì)象。
  • 如果該方法存在,則按該方法的內(nèi)容返回對(duì)象。
  • 如果我們之前沒有實(shí)例化單例對(duì)象,則會(huì)返回null。
好了,到這來終于把坑都踩完了。

還是費(fèi)了不少勁。

不過,我偷偷告訴你一句,其實(shí)還有更簡(jiǎn)單的方法,哈哈哈。

納尼。。。

5 枚舉

其實(shí)在java中枚舉就是天然的單例,每一個(gè)實(shí)例只有一個(gè)對(duì)象,這是java底層內(nèi)部機(jī)制保證的。

簡(jiǎn)單的用法:

public?enum??SimpleSingleton7?{
????INSTANCE;
????
????public?void?doSamething()?{
????????System.out.println("doSamething");
????}
}???
在調(diào)用的地方:

public?class?SimpleSingleton7Test?{

????public?static?void?main(String[]?args)?{
????????SimpleSingleton7.INSTANCE.doSamething();
????}
}
在枚舉中實(shí)例對(duì)象INSTANCE是唯一的,所以它是天然的單例模式。

當(dāng)然,在枚舉對(duì)象唯一性的這個(gè)特性,還能創(chuàng)建其他的單例對(duì)象,例如:

public?enum??SimpleSingleton7?{
????INSTANCE;
????
????private?Student?instance;
????
????SimpleSingleton7()?{
???????instance?=?new?Student();
????}
????
????public?Student?getInstance()?{
???????return?instance;
????}
}

class?Student?{
}
jvm保證了枚舉是天然的單例,并且不存在線程安全問題,此外,還支持序列化。

在java大神Joshua Bloch的經(jīng)典書籍《Effective Java》中說過:

單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法。

6 多例模式

我們之前聊過的單例模式,都只會(huì)產(chǎn)生一個(gè)實(shí)例。但它其實(shí)還有一個(gè)變種,也就是我們接下來要聊的:多例模式。

多例模式顧名思義,它允許創(chuàng)建多個(gè)實(shí)例。但它的初衷是為了控制實(shí)例的個(gè)數(shù),其他的跟單例模式差不多。

具體實(shí)現(xiàn)代碼如下:

public?class?SimpleMultiPattern?{
????//持有自己類的引用
????private?static?final?SimpleMultiPattern?INSTANCE1?=?new?SimpleMultiPattern();
????private?static?final?SimpleMultiPattern?INSTANCE2?=?new?SimpleMultiPattern();

????//私有的構(gòu)造方法
????private?SimpleMultiPattern()?{
????}
????//對(duì)外提供獲取實(shí)例的靜態(tài)方法
????public?static?SimpleMultiPattern?getInstance(int?type)?{
????????if(type?==?1)?{
??????????return?INSTANCE1;
????????}
????????return?INSTANCE2;
????}
}
為了看起來更直觀,我把一些額外的安全相關(guān)代碼去掉了。

有些朋友可能會(huì)說:既然多例模式也是為了控制實(shí)例數(shù)量,那我們常見的池技術(shù),比如:數(shù)據(jù)庫(kù)連接池,是不是通過多例模式實(shí)現(xiàn)的?

答:不,它是通過享元模式實(shí)現(xiàn)的。

那么,多例模式和享元模式有什么區(qū)別?

  • 多例模式:跟單例模式一樣,純粹是為了控制實(shí)例數(shù)量,使用這種模式的類,通常是作為程序某個(gè)模塊的入口。
  • 享元模式:它的側(cè)重點(diǎn)是對(duì)象之間的銜接。它把動(dòng)態(tài)的、會(huì)變化的狀態(tài)剝離出來,共享不變的東西。

7 真實(shí)使用場(chǎng)景

最后,跟大家一起聊聊,單例模式的一些使用場(chǎng)景。我們主要看看在java的框架中,是如何使用單例模式,給有需要的朋友一個(gè)參考。

7.1 Runtime

jdk提供了Runtime類,我們可以通過這個(gè)類獲取系統(tǒng)的運(yùn)行狀態(tài)。

比如可以通過它獲取cpu核數(shù):

int?availableProcessors?=?Runtime.getRuntime().availableProcessors();
Runtime類的關(guān)鍵代碼如下:

public?class?Runtime?{
????private?static?Runtime?currentRuntime?=?new?Runtime();
????
????public?static?Runtime?getRuntime()?{
????????return?currentRuntime;
????}

????private?Runtime()?{}
????...
}
從上面的代碼我們可以看出,這是一個(gè)單例模式,并且是餓漢模式。

但根據(jù)文章之前講過的一些理論知識(shí),你會(huì)發(fā)現(xiàn)Runtime類的這種單例模式實(shí)現(xiàn)方式,顯然不太好。實(shí)例對(duì)象既沒用final關(guān)鍵字修飾,也沒考慮對(duì)象實(shí)例化的性能消耗問題。

不過它的優(yōu)點(diǎn)是實(shí)現(xiàn)起來非常簡(jiǎn)單。

7.2 NamespaceHandlerResolver

spring提供的DefaultNamespaceHandlerResolver是為需要初始化默認(rèn)命名空間處理器,是為了方便后面做標(biāo)簽解析用的。

它的關(guān)鍵代碼如下:

@Nullable
private?volatile?Map?handlerMappings;

private?Map?getHandlerMappings()?{
??Map?handlerMappings?=?this.handlerMappings;
??if?(handlerMappings?==?null)?{
???synchronized?(this)?{
????handlerMappings?=?this.handlerMappings;
????if?(handlerMappings?==?null)?{
?????if?(logger.isDebugEnabled())?{
??????logger.debug("Loading?NamespaceHandler?mappings?from?["? ?this.handlerMappingsLocation? ?"]");
?????}
?????try?{
??????Properties?mappings?=
????????PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation,?this.classLoader);
??????if?(logger.isDebugEnabled())?{
???????logger.debug("Loaded?NamespaceHandler?mappings:?"? ?mappings);
??????}
??????handlerMappings?=?new?ConcurrentHashMap<>(mappings.size());
??????CollectionUtils.mergePropertiesIntoMap(mappings,?handlerMappings);
??????this.handlerMappings?=?handlerMappings;
?????}
?????catch?(IOException?ex)?{
??????throw?new?IllegalStateException(
????????"Unable?to?load?NamespaceHandler?mappings?from?location?["? ?this.handlerMappingsLocation? ?"]",?ex);
?????}
????}
???}
??}
??return?handlerMappings;
?}
我們看到它使用了雙重檢測(cè)鎖,并且還定義了一個(gè)局部變量handlerMappings,這是非常高明之處。

使用局部變量相對(duì)于不使用局部變量,可以提高性能。主要是由于 volatile 變量創(chuàng)建對(duì)象時(shí)需要禁止指令重排序,需要一些額外的操作。

7.3 LogFactory

mybatis提供LogFactory類是為了創(chuàng)建日志對(duì)象,根據(jù)引入的jar包,決定使用哪種方式打印日志。具體代碼如下:

public?final?class?LogFactory?{

??public?static?final?String?MARKER?=?"MYBATIS";

??private?static?Constructor?logConstructor;

??static?{
????tryImplementation(new?Runnable()?{
??????@Override
??????public?void?run()?{
????????useSlf4jLogging();
??????}
????});
????tryImplementation(new?Runnable()?{
??????@Override
??????public?void?run()?{
????????useCommonsLogging();
??????}
????});
????tryImplementation(new?Runnable()?{
??????@Override
??????public?void?run()?{
????????useLog4J2Logging();
??????}
????});
????tryImplementation(new?Runnable()?{
??????@Override
??????public?void?run()?{
????????useLog4JLogging();
??????}
????});
????tryImplementation(new?Runnable()?{
??????@Override
??????public?void?run()?{
????????useJdkLogging();
??????}
????});
????tryImplementation(new?Runnable()?{
??????@Override
??????public?void?run()?{
????????useNoLogging();
??????}
????});
??}

??private?LogFactory()?{
????//?disable?construction
??}

??public?static?Log?getLog(Class?aClass)?{
????return?getLog(aClass.getName());
??}

??public?static?Log?getLog(String?logger)?{
????try?{
??????return?logConstructor.newInstance(logger);
????}?catch?(Throwable?t)?{
??????throw?new?LogException("Error?creating?logger?for?logger?"? ?logger? ?".??Cause:?"? ?t,?t);
????}
??}

??public?static?synchronized?void?useCustomLogging(Class?clazz)?{
????setImplementation(clazz);
??}

??public?static?synchronized?void?useSlf4jLogging()?{
????setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
??}

??public?static?synchronized?void?useCommonsLogging()?{
????setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
??}

??public?static?synchronized?void?useLog4JLogging()?{
????setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
??}

??public?static?synchronized?void?useLog4J2Logging()?{
????setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
??}

??public?static?synchronized?void?useJdkLogging()?{
????setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
??}

??public?static?synchronized?void?useStdOutLogging()?{
????setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
??}

??public?static?synchronized?void?useNoLogging()?{
????setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
??}

??private?static?void?tryImplementation(Runnable?runnable)?{
????if?(logConstructor?==?null)?{
??????try?{
????????runnable.run();
??????}?catch?(Throwable?t)?{
????????//?ignore
??????}
????}
??}

??private?static?void?setImplementation(Class?implClass)?{
????try?{
??????Constructor?candidate?=?implClass.getConstructor(String.class);
??????Log?log?=?candidate.newInstance(LogFactory.class.getName());
??????if?(log.isDebugEnabled())?{
????????log.debug("Logging?initialized?using?'"? ?implClass? ?"'?adapter.");
??????}
??????logConstructor?=?candidate;
????}?catch?(Throwable?t)?{
??????throw?new?LogException("Error?setting?Log?implementation.??Cause:?"? ?t,?t);
????}
??}
}
這段代碼非常經(jīng)典,但它卻是一個(gè)不走尋常路的單例模式。因?yàn)樗鼊?chuàng)建的實(shí)例對(duì)象,可能存在多種情況,根據(jù)引入不同的jar包,加載不同的類創(chuàng)建實(shí)例對(duì)象。如果有一個(gè)創(chuàng)建成功,則用它作為整個(gè)類的實(shí)例對(duì)象。

這里有個(gè)非常巧妙的地方是:使用了很多tryImplementation方法,方便后面進(jìn)行擴(kuò)展。不然要寫很多,又臭又長(zhǎng)的if...else判斷。

此外,它跟常規(guī)的單例模式的區(qū)別是,LogFactory類中定義的實(shí)例對(duì)象是Log類型,并且getLog方法返回的參數(shù)類型也是Log,不是LogFactory。

最關(guān)鍵的一點(diǎn)是:getLog方法中是通過構(gòu)造器的newInstance方法創(chuàng)建的實(shí)例對(duì)象,每次請(qǐng)求getLog方法都會(huì)返回一個(gè)新的實(shí)例,它其實(shí)是一個(gè)多例模式。

7.4 ErrorContext

mybatis提供ErrorContext類記錄了錯(cuò)誤信息的上下文,方便后續(xù)處理。

那么它是如何實(shí)現(xiàn)單例模式的呢?關(guān)鍵代碼如下:

public?class?ErrorContext?{
??...
??private?static?final?ThreadLocal?LOCAL?=?new?ThreadLocal();
??
??private?ErrorContext()?{
??}
??
??public?static?ErrorContext?instance()?{
????ErrorContext?context?=?LOCAL.get();
????if?(context?==?null)?{
??????context?=?new?ErrorContext();
??????LOCAL.set(context);
????}
????return?context;
??}
??...
}??
我們可以看到,ErrorContext跟傳統(tǒng)的單例模式不一樣,它改良了一下。它使用了餓漢模式,并且使用ThreadLocal,保證每個(gè)線程中的實(shí)例對(duì)象是單例的。這樣看來,ErrorContext類創(chuàng)建的對(duì)象不是唯一的,它其實(shí)也是多例模式的一種。

7.5 spring的單例

以前在spring中要定義一個(gè)bean,需要在xml文件中做如下配置:

"test"?class="com.susan.Test"?init-method="init"?scope="singleton">
在bean標(biāo)簽上有個(gè)scope屬性,我們可以通過指定該屬性控制bean實(shí)例是單例的,還是多例的。如果值為singleton,代表是單例的。當(dāng)然如果該參數(shù)不指定,默認(rèn)也是單例的。如果值為prototype,則代表是多例的。

在spring的AbstractBeanFactory類的doGetBean方法中,有這樣一段代碼:

if?(mbd.isSingleton())?{
????sharedInstance?=?getSingleton(beanName,?()?->?{
??????return?createBean(beanName,?mbd,?args);
??});
??bean?=?getObjectForBeanInstance(sharedInstance,?name,?beanName,?mbd);
}?else?if?(mbd.isPrototype())?{
????Object?prototypeInstance?=?createBean(beanName,?mbd,?args);
????bean?=?getObjectForBeanInstance(prototypeInstance,?name,?beanName,?mbd);
}?else?{
????....
}
這段代碼我為了好演示,看起來更清晰,我特地簡(jiǎn)化過的。它的主要邏輯如下:

  1. 判斷如果scope是singleton,則調(diào)用getSingleton方法獲取實(shí)例。
  2. 如果scope是prototype,則直接創(chuàng)建bean實(shí)例,每次會(huì)創(chuàng)建一個(gè)新實(shí)例。
  3. 如果scope是其他值,則允許我們自定bean的創(chuàng)建過程。
其中g(shù)etSingleton方法主要代碼如下:

public?Object?getSingleton(String?beanName,?ObjectFactory?singletonFactory)?{
??Assert.notNull(beanName,?"Bean?name?must?not?be?null");
??synchronized?(this.singletonObjects)?{
???Object?singletonObject?=?this.singletonObjects.get(beanName);
???if?(singletonObject?==?null)?{
??????????singletonObject?=?singletonFactory.getObject();
?????????if?(newSingleton)?{
???????????addSingleton(beanName,?singletonObject);
????????}
???}
???return?singletonObject;
??}
}

有個(gè)關(guān)鍵的singletonObjects對(duì)象,其實(shí)是一個(gè)ConcurrentHashMap集合:

private?final?Map?singletonObjects?=?new?ConcurrentHashMap<>(256);
getSingleton方法的主要邏輯如下:

  1. 根據(jù)beanName先從singletonObjects集合中獲取bean實(shí)例。
  2. 如果bean實(shí)例不為空,則直接返回該實(shí)例。
  3. 如果bean實(shí)例為空,則通過getObject方法創(chuàng)建bean實(shí)例,然后通過addSingleton方法,將該bean實(shí)例添加到singletonObjects集合中。
  4. 下次再通過beanName從singletonObjects集合中,就能獲取到bean實(shí)例了。
在這里spring是通過ConcurrentHashMap集合來保證對(duì)象的唯一性。

最后留給大家?guī)讉€(gè)小問題思考一下:

1. 多例模式 和 多對(duì)象模式有什么區(qū)別?2. java框架中有些單例模式用的不規(guī)范,我要參考不?3. spring的單例,只是結(jié)果是單例的,但完全沒有遵循單例模式的固有寫法,它也算是單例模式嗎?
歡迎大家給我留言,說出你心中的答案。

碼字不易,如果讀了文章有些收獲的話,請(qǐng)幫我點(diǎn)贊一下,謝謝你的支持和鼓勵(lì)。

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
關(guān)閉
關(guān)閉