1.單例模式概述
(1)引言
單例模式是應(yīng)用最廣的模式之一,也是23種設(shè)計(jì)模式中最基本的一個(gè)。本文旨在總結(jié)通過(guò)Java實(shí)現(xiàn)單例模式的各個(gè)版本的優(yōu)缺點(diǎn)及適用場(chǎng)景,詳細(xì)分析如何實(shí)現(xiàn)線程安全的單例模式,并探討單例模式的一些擴(kuò)展。
(2)單例模式的定義
??Ensure a class has only one instance,and provide a global point of access to it.(確保某一個(gè)類只有一個(gè)實(shí)例,并且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例)
通用類圖為:

Singleton類稱為單例類,通過(guò)使用private的構(gòu)造函數(shù)確保了在一個(gè)應(yīng)用中只產(chǎn)生一個(gè)實(shí)例,并且是自行實(shí)例化的(在Singleton中自己使用new Singleton())。
(3)使用場(chǎng)景
在一個(gè)系統(tǒng)中,要求一個(gè)類有且僅有一個(gè)對(duì)象,如果出現(xiàn)多個(gè)對(duì)象就會(huì)出現(xiàn)“不良反應(yīng)”,可以采用單例模式,具體的場(chǎng)景如下:
- 要求生成唯一序列號(hào)的環(huán)境;
- 在整個(gè)項(xiàng)目中需要一個(gè)共享訪問(wèn)點(diǎn)或共享數(shù)據(jù),例如一個(gè)Web頁(yè)面上的計(jì)數(shù)器,可以不用把每次刷新都記錄到數(shù)據(jù)庫(kù)中,使用單例模式保持計(jì)數(shù)器的值,并確保是線程安全的;
- 創(chuàng)建一個(gè)對(duì)象需要消耗的資源過(guò)多,如要訪問(wèn)IO和數(shù)據(jù)庫(kù)等資源;
- 需要定義大量的靜態(tài)常量和靜態(tài)方法(如工具類)的環(huán)境,可以采用單例模式(當(dāng)然,也可以直接聲明為static的方式)。
(4)優(yōu)缺點(diǎn)
單例模式的優(yōu)點(diǎn)
- 由于單例模式在內(nèi)存中只有一個(gè)實(shí)例,減少了內(nèi)存開(kāi)支,特別是一個(gè)對(duì)象需要頻繁地創(chuàng)建銷毀時(shí),而且創(chuàng)建或銷毀時(shí)性能又無(wú)法優(yōu)化,單例模式的優(yōu)勢(shì)就非常明顯;
- 由于單例模式只生成一個(gè)實(shí)例,所以減少了系統(tǒng)的性能開(kāi)銷,當(dāng)一個(gè)對(duì)象的產(chǎn)生需要比較多的資源的時(shí)候,如讀取配置,產(chǎn)生其他的依賴對(duì)象時(shí),可以通過(guò)在應(yīng)用啟動(dòng)的時(shí)候直接產(chǎn)生一個(gè)單例對(duì)象,然后用永久駐留內(nèi)存的方式來(lái)解決;
- 單例模式可以避免對(duì)資源的多重占用,例如對(duì)一個(gè)寫文件動(dòng)作,由于只有一個(gè)實(shí)例存在內(nèi)存中,避免對(duì)同一個(gè)資源文件的同時(shí)寫操作;
- 單例模式可以在系統(tǒng)設(shè)置全局的訪問(wèn)點(diǎn),優(yōu)化和共享資源訪問(wèn),例如可以設(shè)計(jì)一個(gè)單例類,負(fù)責(zé)所有數(shù)據(jù)表的映射處理
單例模式的缺點(diǎn)
- 單例模式一般沒(méi)有接口,擴(kuò)展困難,若要擴(kuò)展,除了修改代碼基本上沒(méi)有第二種途徑可以實(shí)現(xiàn);
- 單例模式與單一職責(zé)原則有沖突,一個(gè)類應(yīng)該只實(shí)現(xiàn)一個(gè)邏輯,而不關(guān)心他是否是單例的,是不是要單例取決于環(huán)境,單例模式把“要單例”和業(yè)務(wù)邏輯融合在一個(gè)類中。
2.最基本的實(shí)現(xiàn)方式
代碼實(shí)現(xiàn)為:
public class Singleton {
private static Singleton singleton;
// 限制產(chǎn)生多個(gè)對(duì)象
private Singleton() {
}
// 獲得對(duì)象實(shí)例的方法
public static Singleton getSingleton() {
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
相信大多數(shù)同學(xué)在入門Java的階段都見(jiàn)過(guò)這段代碼。該方式在低并發(fā)的情況下尚不會(huì)出現(xiàn)問(wèn)題,若系統(tǒng)壓力增大,并發(fā)量增加時(shí)則可能在內(nèi)存中出現(xiàn)多個(gè)實(shí)例,破壞設(shè)計(jì)的初衷。本文的后續(xù)就是圍繞這種實(shí)現(xiàn)分析改進(jìn),探討實(shí)現(xiàn)線程安全的單例模式的最佳實(shí)踐。
??為什么這種實(shí)現(xiàn)是線程不安全的呢?如一個(gè)線程A執(zhí)行到singleton = new Singleton();這里,但還沒(méi)有獲得對(duì)象(對(duì)象初始化是需要時(shí)間的),第二個(gè)線程B也在執(zhí)行,執(zhí)行到if(singleton == null)判斷,那么線程B獲得判斷條件也是為真,于是繼續(xù)運(yùn)行下去,線程A獲得了一個(gè)對(duì)象,線程B也獲得了一個(gè)對(duì)象,在內(nèi)存中就出現(xiàn)兩個(gè)對(duì)象,造成單例模式的失效??!
??所以根本原因在于可能存在多個(gè)線程并發(fā)的訪問(wèn)getSingleton()方法造成單例對(duì)象的多次創(chuàng)建,解決因多線程并發(fā)訪問(wèn)導(dǎo)致單例模式實(shí)效的最佳方法就是--不要使用多線程并發(fā)訪問(wèn)。(⊙o⊙)…

3.餓漢式
(1)實(shí)現(xiàn)原理
言歸正傳,上面說(shuō)的問(wèn)題其實(shí)就是對(duì)if(singleton == null)的判斷失效造成singleton = new Singleton();可能會(huì)被多個(gè)線程并發(fā)的執(zhí)行。餓漢式單例模式的實(shí)現(xiàn)的本質(zhì)其實(shí)就是依賴類加載機(jī)制保證構(gòu)造方法只會(huì)被執(zhí)行一次。JVM在類的初始化階段(即在Class被加載后,且被線程使用之前),會(huì)執(zhí)行類的初始化。在執(zhí)行類的初始化期間,JVM會(huì)去獲取一個(gè)鎖。這個(gè)鎖可以同步多個(gè)線程對(duì)同一個(gè)類的初始化。
餓漢式單例的實(shí)現(xiàn)代碼為:
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {
}
// 獲得對(duì)象實(shí)例的方法
public static Singleton getSingleton() {
return singleton;
}
}
(2)優(yōu)缺點(diǎn)及適用場(chǎng)景
可以看到餓漢式的實(shí)現(xiàn)非常簡(jiǎn)單,適合那些在初始化時(shí)就要用到單例的情況,如果單例對(duì)象初始化非???,而且占用內(nèi)存非常小的時(shí)候這種方式是比較合適的,可以直接在應(yīng)用啟動(dòng)時(shí)加載并初始化。
不適用的場(chǎng)景:
- 單例初始化的操作耗時(shí)比較長(zhǎng)而應(yīng)用對(duì)于啟動(dòng)速度又有要求;
- 單例的占用內(nèi)存比較大;
- 單例只是在某個(gè)特定場(chǎng)景的情況下才會(huì)被使用,而一般情況下是不會(huì)使用的;
在上述的幾種情況下使用餓漢式的單例模式是不合適的,這時(shí)候就需要用到懶漢式的方式去按需延遲加載單例。
4.利用同步鎖機(jī)制實(shí)現(xiàn)的懶漢式
實(shí)現(xiàn)代碼為:
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
// 獲得對(duì)象實(shí)例的方法
public static Singleton getSingleton() {
synchronized(Singleton.class) {
if(singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
}
這種是最常見(jiàn)的懶漢式單例實(shí)現(xiàn),使用同步鎖synchronized(Singleton.class)防止多線程同時(shí)進(jìn)入造成instance被多次實(shí)例化。但是他的缺陷也是非常明顯的,就是每次在調(diào)用getSingleton()獲取單例的實(shí)例的時(shí)候,都需要進(jìn)行同步。事實(shí)上我們只想保證一次初始化成功,其余的快速返回而已,如果在getInstance頻繁使用的地方就要考慮重新優(yōu)化了。
5.對(duì)同步鎖機(jī)制實(shí)現(xiàn)懶漢式的改進(jìn)--DCL
(1)原理與實(shí)現(xiàn)
由于synchronized(甚至是無(wú)競(jìng)爭(zhēng)的synchronized)存在著巨大的性能開(kāi)銷。因此,人們想出了一個(gè)“聰明”的技巧:雙重檢查鎖定(double-checked locking)。通過(guò)這種方式來(lái)降低同步的開(kāi)銷。下面是使用雙重檢查鎖定來(lái)實(shí)現(xiàn)延遲初始化的代碼實(shí)現(xiàn):
public class Singleton {
private static Singleton instance = null; //1
// 獲得對(duì)象實(shí)例的方法
public static Singleton getSingleton() { //2
if(instance == null) { //3:第一次檢查
synchronized(Singleton.class) { //4:加鎖
if(instance == null) //5:第二次檢查
instance = new Singleton(); //6:問(wèn)題的根源產(chǎn)生
}
}
return instance;
}
private Singleton() {
}
}
如上面的代碼所示,它的“優(yōu)點(diǎn)”如下:
- 在多個(gè)線程試圖在同一時(shí)間創(chuàng)建對(duì)象時(shí),會(huì)通過(guò)加鎖來(lái)保證只有一個(gè)線程能創(chuàng)建對(duì)象;
- 在對(duì)象創(chuàng)建好了以后,執(zhí)行g(shù)etSingleton()方法將不需要獲取鎖,直接返回已經(jīng)創(chuàng)建好的對(duì)象
雙重檢查模式看上去好像很完美,但這是一個(gè)錯(cuò)誤的優(yōu)化,在線程執(zhí)行到上面所示的代碼3處讀取到singleton對(duì)象不為null時(shí),singleton引用的對(duì)象可能還沒(méi)有完成初始化。
(2)問(wèn)題分析
可能產(chǎn)生錯(cuò)誤的場(chǎng)景
- 1.線程A進(jìn)入getSingleton()方法;
- 2.因?yàn)榇藭r(shí)instance為null,所以線程A進(jìn)入synchronized塊;
- 3.線程A執(zhí)行 instance = new Singleton(); 把實(shí)例變量instance設(shè)置成了非空。(注意,是在調(diào)用構(gòu)造方法之前)
- 4.線程A退出,線程B進(jìn)入。
- 5.線程B檢查instance是否為空,此時(shí)不為空(第三步的時(shí)候被線程A設(shè)置成了非空)。線程B返回instance的引用。(問(wèn)題出現(xiàn)了,這時(shí)instance的引用并不是Singleton的實(shí)例,因?yàn)闆](méi)有調(diào)用構(gòu)造方法)
- 6.線程B退出,線程A進(jìn)入;
- 7.線程A繼續(xù)調(diào)用構(gòu)造方法,完成instance的初始化,再返回。
(3)問(wèn)題根源
多線程問(wèn)題,很大程度是由于非原子性造成的,如果我們每一個(gè)可能產(chǎn)生競(jìng)爭(zhēng)的地方都是原子性的,那多線程需要考慮的東西就要少很多了。在上述程序中也是一樣,我們看第六行代碼:
instance = new Singleton(); //6:問(wèn)題的根源產(chǎn)生
在JMM中,這行代碼可以分解為3個(gè)過(guò)程:
memory = allocate(); //#1為對(duì)象分配內(nèi)存空間
init(memory); //#2初始化
instance = memory; //#3設(shè)置instance,將其指向剛分配的內(nèi)存空間。
上面的3行代碼,如果是順序執(zhí)行,不會(huì)帶來(lái)問(wèn)題。但是,在某些JIT編譯器上,#2和#3可能發(fā)生重排序。也就是說(shuō),重排序后,上面三個(gè)過(guò)程變成了:
memory = allocate(); //#1為對(duì)象分配內(nèi)存空間
instance = memory; //#2
init(memory); //#3初始化
根據(jù)《The Java Language Specification, Java SE 7 Edition》一書中的內(nèi)容:所有線程在執(zhí)行java程序時(shí)必須要遵守intra-thread semantics。intra-thread semantics保證重排序不會(huì)改變單線程內(nèi)的程序執(zhí)行結(jié)果。換句話來(lái)說(shuō),intra-thread semantics允許那些在單線程內(nèi),不會(huì)改變單線程程序執(zhí)行結(jié)果的重排序。上面三行偽代碼的2和3之間雖然被重排序了,但這個(gè)重排序并不會(huì)違反intra-thread semantics。這個(gè)重排序在沒(méi)有改變單線程程序的執(zhí)行結(jié)果的前提下,可以提高程序的執(zhí)行性能。
??下面,再讓我們看看多線程并發(fā)執(zhí)行的時(shí)候的情況。請(qǐng)看下面的示意圖:

這里2和3雖然重排序了,但java內(nèi)存模型的intra-thread semantics將確保2一定會(huì)排在4前面執(zhí)行。因此線程A的intra-thread semantics沒(méi)有改變。但2和3的重排序,將導(dǎo)致線程B在B1處判斷出instance不為空,線程B接下來(lái)將訪問(wèn)instance引用的對(duì)象。此時(shí),線程B將會(huì)訪問(wèn)到一個(gè)還未初始化的對(duì)象。。
分析清楚問(wèn)題發(fā)生的根源之后,可以想出兩個(gè)辦法來(lái)實(shí)現(xiàn)線程安全的延遲初始化:
- 不允許2和3重排序;
- 允許2和3重排序,但不允許其他線程“看到”這個(gè)重排序。
后文介紹的解決方案就分別對(duì)應(yīng)于上面這兩點(diǎn)。
6.Java1.5以后安全的DCL版本
(1)實(shí)現(xiàn)代碼
public class Singleton {
private volatile static Singleton instance = null;
// 獲得對(duì)象實(shí)例的方法
public static Singleton getSingleton() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null)
instance = new Singleton();
}
}
return instance;
}
private Singleton() {
}
}
(2)原理分析
可以發(fā)現(xiàn)代碼只做一點(diǎn)小的修改(把instance聲明為volatile型),為什么volatile可以解決呢?回顧一下他的兩層語(yǔ)義:
- (1)可見(jiàn)性:指的是在一個(gè)線程中對(duì)該變量的修改會(huì)馬上由工作內(nèi)存(Work Memory)寫回主內(nèi)存(Main Memory)
- (2)禁止指令重排序優(yōu)化
當(dāng)聲明對(duì)象的引用為volatile后,“問(wèn)題的根源”的三行偽代碼中的2和3之間的重排序,在多線程環(huán)境中將會(huì)被禁止,從而在根本上解決了問(wèn)題。但是很不幸,禁止指令重排優(yōu)化這條語(yǔ)義直到j(luò)dk1.5以后才能正確工作。此前的JDK中即使將變量聲明為volatile也無(wú)法完全避免重排序所導(dǎo)致的問(wèn)題。所以,在jdk1.5版本前,雙重檢查鎖形式的單例模式是無(wú)法保證線程安全的。
??寫到這里,可能有的同學(xué)會(huì)有疑問(wèn)了:

7.Java1.4以前安全的DCL版本
額(⊙o⊙)…雖然現(xiàn)在的日常開(kāi)發(fā)已經(jīng)普遍在使用Java1.7甚至1.8了。不過(guò)嘗試著探討下在Jav1.4以前實(shí)現(xiàn)安全的DCL還是一個(gè)還有意思的話題。我自己也沒(méi)有找到太確定的答案,這方面的資料也非常的少。下面給出的實(shí)現(xiàn)代碼不一定能保證正確,貼出來(lái)僅供參考,歡迎有興趣的同學(xué)在評(píng)論區(qū)留言分享一下經(jīng)驗(yàn)。

(2)實(shí)現(xiàn)代碼及思路
public class Singleton {
private static Singleton instance = null;
// 獲得對(duì)象實(shí)例的方法
public static Singleton getSingleton() {
if(instance == null) { //1.第一次檢查
synchronized(Singleton.class) { //2.第一個(gè)synchronized塊
Singleton temp = instance; //3.給臨時(shí)變量temp賦值
if(temp == null) { //4.第二次檢查
synchronized(Singleton.class) { //5.第二個(gè)synchronized塊
temp = new Singleton(); //6.解決問(wèn)題的關(guān)鍵地方
}
instance = temp; //7.把temp的引用賦值給instance
}
}
}
return instance;
}
private Singleton() {
}
}
上面給出的代碼中,很關(guān)鍵的地方在于在synchronized塊中引入了一個(gè)臨時(shí)變量Singleton temp,通過(guò)對(duì)temp的判空及相應(yīng)的初始化,保證在代碼7處,執(zhí)行intance = temp;時(shí),instance不為null且完成了初始化。
8.內(nèi)部類方式
(1)實(shí)現(xiàn)
在第五部分的結(jié)尾我們提到了兩個(gè)辦法來(lái)實(shí)現(xiàn)線程安全的延遲初始化,內(nèi)部類方式正是基于第二種方法--線程之間重排序透明性
實(shí)現(xiàn)代碼為:
public class Singleton {
// 獲得對(duì)象實(shí)例的方法
public static Singleton getSingleton() {
return SingletonHolder.instance;
}
/**
* 靜態(tài)內(nèi)部類與外部類的實(shí)例沒(méi)有綁定關(guān)系,而且只有被調(diào)用時(shí)才會(huì)
* 加載,從而實(shí)現(xiàn)了延遲加載
*/
private static class SingletonHolder {
/**
* 靜態(tài)初始化器,由JVM來(lái)保證線程安全
*/
private static Singleton instance = new Singleton();
}
private Singleton() {
}
}
(2)原理分析
在這種方式中,使用了一個(gè)專門的內(nèi)部類來(lái)初始化Singleton,JVM將推遲SingletonHolder的初始化操作,直到開(kāi)始使用這個(gè)類時(shí)才初始化。并且在初始化的過(guò)程中JVM會(huì)去獲取一個(gè)用于同步多個(gè)線程對(duì)同一個(gè)類進(jìn)行初始化的鎖,這樣就不需要額外的同步。這種方式不僅能夠保證線程安全,也能保證單例對(duì)象的唯一性,同時(shí)也延遲實(shí)例化,是一種非常推薦的方式。
9.枚舉方式
(1)實(shí)現(xiàn)方法
從Java1.5起,可以通過(guò)使用枚舉機(jī)制來(lái)實(shí)現(xiàn)單例模式:
public enum Singleton {
// 定義枚舉元素,他就是Singleton的一個(gè)實(shí)例
INSTANCE;
public void doSomething() {
// do something
}
}
調(diào)用方式
Singleton singleton = Singleton.INSTANCE;
singleton.doSomething();
可以看到實(shí)現(xiàn)的代碼非常的簡(jiǎn)潔,按照J(rèn)oshua Bloch大神的原話來(lái)說(shuō):
While this approach has yet to be widely adopted,a single-element enum type is the best way to implement a singleton.
(2)序列化與反序列化的問(wèn)題
在上述的幾種單例模式實(shí)現(xiàn)中,在一個(gè)情況下它們會(huì)出現(xiàn)重新創(chuàng)建對(duì)象的情況,那就是反序列化。
??通過(guò)序列化可以將一個(gè)單例的實(shí)例對(duì)象寫到磁盤,然后再讀回來(lái),從而有效地獲得一個(gè)實(shí)例。即使構(gòu)造函數(shù)是私有的,反序列化時(shí)依然可以通過(guò)特殊的途徑去創(chuàng)建類的一個(gè)新的實(shí)例,相當(dāng)于調(diào)用該類的構(gòu)造函數(shù)。反序列化操作提供一個(gè)很特別的鉤子函數(shù),類中具有一個(gè)私有的、被實(shí)例化的方法readResolve(),這個(gè)方法可以讓開(kāi)發(fā)人員控制對(duì)象的反序列化。例如,上述幾個(gè)實(shí)例中如果要杜絕單例對(duì)象在被反序列化時(shí)重新生成對(duì)象,那么必須加入如下方法:
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
也就是在readResolve方法中將實(shí)例對(duì)象返回,而不是默認(rèn)的重新生成一個(gè)新的對(duì)象。
(3)Java反射攻擊
下面我們基于內(nèi)部類實(shí)現(xiàn)的單例模式的方式,來(lái)演示一下通過(guò)JAVA的反射機(jī)制來(lái)“攻擊”單例模式:
public class TestMain {
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
Class<?> classType = Singleton.class;
Constructor<?> c = classType.getDeclaredConstructor(null);
c.setAccessible(true);
Singleton singleton1 = (Singleton) c.newInstance();
Singleton singleton2 = Singleton.getSingleton();
System.out.println(singleton1 == singleton2);
}
}
運(yùn)行結(jié)果:false,可以看到,通過(guò)反射獲取構(gòu)造函數(shù),然后調(diào)用setAccessible(true)就可以調(diào)用私有的構(gòu)造函數(shù),所有singleton1和singleton2是兩個(gè)不同的對(duì)象。如果要抵御這種攻擊,可以修改構(gòu)造器,讓它在被要求創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋出異常。
修改原有代碼為:
public class Singleton {
public static Singleton getSingleton() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
private static boolean flag = false;
private Singleton() {
synchronized(Singleton.class) {
if(flag == false) {
flag = !flag;
} else {
throw new RuntimeException("單例模式被破壞!");
}
}
}
}
再次運(yùn)行上面的測(cè)試代碼:得到的結(jié)果為:
Exception in thread "main" java.lang.RuntimeException: 單例模式被破壞!
at com.danli.Singleton.<init>(Singleton.java:29)
at com.danli.Singleton.getSingleton(Singleton.java:12)
at com.danli.TestMain.main(TestMain.java:23)
可以看到,成功的阻止了單例模式被破壞。
但是我們?nèi)绻苯踊诿杜e方式實(shí)現(xiàn)的單例模式進(jìn)行同樣的代碼測(cè)試,會(huì)直接得到結(jié)果:
Exception in thread "main" java.lang.NoSuchMethodException: com.danli.Singleton.<init>()
at java.lang.Class.getConstructor0(Class.java:2730)
at java.lang.Class.getDeclaredConstructor(Class.java:2004)
at com.danli.TestMain.main(TestMain.java:20)
可以看到,枚舉方式實(shí)現(xiàn)的單例自己是可以避免反射攻擊的。
(4)枚舉方式的優(yōu)點(diǎn)
餓漢式、懶漢式、雙重校驗(yàn)鎖(DCL)還是靜態(tài)內(nèi)部類都存在的缺點(diǎn):
- 都需要額外的工作(Serializable、transient、readResolve())來(lái)實(shí)現(xiàn)序列化,否則每次反序列化一個(gè)序列化的對(duì)象實(shí)例時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例。
- 可能會(huì)有人使用反射強(qiáng)行調(diào)用我們的私有構(gòu)造器(如果要避免這種情況,可以修改構(gòu)造器,讓它在創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋異常)。
枚舉類很好的解決了這兩個(gè)問(wèn)題,使用枚舉除了線程安全和防止反射強(qiáng)行調(diào)用構(gòu)造器之外,還提供了自動(dòng)序列化機(jī)制,防止反序列化的時(shí)候創(chuàng)建新的對(duì)象。因此,《EffectiveJava》Item3中推薦盡可能地使用枚舉來(lái)實(shí)現(xiàn)單例。
??但是在Android中卻不推薦這種用法,在Android官網(wǎng)Manage Your App's Memory中有這樣一段話:
- Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
意思就是枚舉類這種寫法雖然簡(jiǎn)單方便,但是內(nèi)存占用上是靜態(tài)變量的兩倍以上,所以盡可能的避免這種寫法。
??不過(guò)網(wǎng)上有的建議是如果程序不是大量采用枚舉,那么這種性能的體現(xiàn)是很小的,基本不會(huì)受到影響,不用特別在意。如果程序出現(xiàn)了性能問(wèn)題,理論上這個(gè)地方就是一個(gè)性能優(yōu)化點(diǎn)。
10.單例模式的擴(kuò)展
(1)定義
上文的幾種實(shí)現(xiàn)方式里,一個(gè)類都只產(chǎn)生一個(gè)對(duì)象。萬(wàn)一有天產(chǎn)品提的需求中,需要一個(gè)類只產(chǎn)能產(chǎn)生兩三個(gè)對(duì)象呢?該怎么實(shí)現(xiàn)?

這種需要產(chǎn)生固定數(shù)量對(duì)象的模式就叫做多例模式,實(shí)際上就是單例模式的自然推廣,作為對(duì)象的創(chuàng)建模式,多例模式有以下的特點(diǎn):
- 多例類可有多個(gè)實(shí)例;
- 多例類必須自己創(chuàng)建,管理自己的實(shí)例,并向外界提供自己的實(shí)例。
(2)應(yīng)用實(shí)例
喜歡打麻將的同學(xué)(捂臉)都知道,每一桌麻將牌局都需要兩個(gè)骰子,因此骰子就應(yīng)該是多例類,這里就以這個(gè)場(chǎng)景為例來(lái)說(shuō)明多例模式的應(yīng)用。
實(shí)現(xiàn)代碼為:
public class Die {
private static Die die1 = new Die();
private static Die die2 = new Die();
private Die() {
}
public static Die getInstance(int whichOne) {
if(whichOne == 1) {
return die1;
} else {
return die2;
}
}
public synchronized int dice() {
Random rand = new Random(System.currentTimeMillis());
int value = rand.nextInt(6);
value += 1;
return value;
}
}
在多例類Die中,使用了餓漢式方式創(chuàng)建了兩個(gè)Die的實(shí)例,根據(jù)靜態(tài)工廠方法的參數(shù),工廠方法返還兩個(gè)實(shí)例中的一個(gè),Die對(duì)象調(diào)用die()方法代表擲骰子,這個(gè)方法會(huì)返還一個(gè)1--6之間的隨機(jī)數(shù),相當(dāng)于骰子的點(diǎn)數(shù)。
(3)實(shí)踐原則
一個(gè)多例類可以使用靜態(tài)變量存儲(chǔ)所有的實(shí)例,特別是實(shí)例數(shù)目不多的時(shí)候,可以使用一個(gè)個(gè)的靜態(tài)變量存儲(chǔ)一個(gè)個(gè)的實(shí)例。當(dāng)數(shù)目較多的時(shí)候,就需要使用Map等集合存儲(chǔ)這些實(shí)例。
??使用這種模式可以讓我們?cè)谠O(shè)計(jì)時(shí)決定在內(nèi)存中有多少個(gè)實(shí)例,方便系統(tǒng)進(jìn)行擴(kuò)展,修正單例可能存在的性能問(wèn)題,提高系統(tǒng)的相應(yīng)速度。例如讀取文件,我們可以在系統(tǒng)啟動(dòng)時(shí)完成初始化工作,在內(nèi)存中啟動(dòng)固定數(shù)量的reader實(shí)例,然后在需要讀取文件時(shí)就可以快速響應(yīng)。
11.小結(jié)
最后總結(jié)一下,不管哪種方案,時(shí)刻牢記單例模式的三大要點(diǎn):
- 線程安全
- 延遲加載
- 序列化與反序列化安全
本文詳細(xì)的分析了懶漢式,餓漢式,雙重檢查鎖定,靜態(tài)內(nèi)部類,枚舉五種方式的具體實(shí)現(xiàn)原理和優(yōu)缺點(diǎn),并簡(jiǎn)要介紹了單例模式的擴(kuò)展--多例模式。希望大家看完之后能對(duì)單例模式有進(jìn)一步的了解,并在日常工作中結(jié)合具體需求選擇適合的單例模式實(shí)現(xiàn)。
參考文獻(xiàn)
1.Double-checked locking and the Singleton pattern
2.雙重檢查鎖定與延遲初始化
3.如何防止單例模式被JAVA反射攻擊
4.Android設(shè)計(jì)模式之單例模式
5.單例模式的一些注意點(diǎn)
6.《設(shè)計(jì)模式之禪》第7章相關(guān)內(nèi)容
7.《Java與模式》第16--18章相關(guān)內(nèi)容
8.《Head First 設(shè)計(jì)模式》第5章相關(guān)內(nèi)容