0. 序言
- 我們要熟練掌握單例模式保檐。不管是實戰(zhàn)開發(fā)中拒贱,還是面試手寫設(shè)計模式中宛徊,都少不了它。
- 通過閱讀本篇博文逻澳,你會了解常用的單例模式闸天,單例模式三要素,以及如何保證單例模式的安全性斜做。
1. 定義
- 保證一個類僅有一個實例苞氮,并提供一個訪問它的全局訪問點。
2. UML類圖
3. 通用代碼(餓漢式)
public class Singleton {
private static final Singleton sSingleton = new Singleton();
//限制產(chǎn)生多個對象
private Singleton() {
}
//通過該方法獲得實例對象
public static Singleton getSingleton() {
return sSingleton;
}
//類中其他方法瓤逼,盡量是static
public static void doSometing() {
}
}
4. 三要素(非常重要)
- 私有構(gòu)造函數(shù)笼吟。
- 暴露公有靜態(tài)方法返回單例類唯一對象。
- 在多線程環(huán)境下確保單例類對象有且只有一個霸旗。
5. 優(yōu)點
- 只有一個實例贷帮,減少內(nèi)存開支。(頻繁創(chuàng)建時)
- 只生成一個實例诱告,減少系統(tǒng)性能開銷撵枢。(一個對象需要比較多的資源時)
- 避免對資源的多重占用,以免對同一個資源文件同時操作精居。(比如:寫文件時)
- 優(yōu)化和共享資源訪問锄禽。(可以在系統(tǒng)設(shè)置全局的訪問點)
6. 缺點
- 沒有接口,擴展困難箱蟆,只能修改代碼沟绪。疑問:為什么不增加接口呢?因為單例模式要求“自行實例化”空猜,接口對單例模式?jīng)]有意義绽慈。
- 對測試不利恨旱。并行開發(fā)環(huán)境中,單例模式?jīng)]有完成坝疼,是不能進行測試的搜贤。
- 單例模式與單一職責(zé)原則有沖突。后者規(guī)定一個類應(yīng)該只實現(xiàn)一個邏輯钝凶,是不是單例取決于環(huán)境仪芒。前者規(guī)定必須是單例而且沒有規(guī)定只能有一個邏輯。
7. 使用場景
- 當(dāng)要求一個類有且只有一個對象耕陷,出現(xiàn)多個對象就會發(fā)生“不良反應(yīng)”時掂名,比如訪問I/O或者數(shù)據(jù)庫等資源。
- 整個項目需要一個共享訪問點或者共享數(shù)據(jù)哟沫。
- 工具類對象饺蔑。
8. 注意事項(一)
在高并發(fā)的情況下,餓漢式不會出現(xiàn)產(chǎn)生多個實例的情況嗜诀,但是懶漢式就要注意線程的同步問題猾警,懶漢式代碼如下:
public class Singleton {
private static Singleton sSingleton = null;
//限制產(chǎn)生多個對象
private Singleton() {
}
//通過該方法獲得實例對象
public static Singleton getSingleton() {
if (sSingleton == null) {
sSingleton = new Singleton();
}
return sSingleton;
}
}
該單例模式在低并發(fā)的情況下并不會出現(xiàn)問題,若并發(fā)量增大則可能出現(xiàn)多個實例隆敢!為什么會這樣呢发皿?
如一個線程A執(zhí)行到sSingleton = new Singleton(),但是沒有獲得對象(對象初始化是需要時間的)拂蝎,第二個線程B也在執(zhí)行穴墅,執(zhí)行到(sSingleton == null)判斷,那么線程B獲得判斷條件也是真温自,于是繼續(xù)運行下去封救,線程A獲得了一個對象,線程B也獲得了一個對象捣作,在內(nèi)存中就出現(xiàn)兩個對象。
解決線程不安全的方法有很多鹅士,可以在getSingleton方法前加sychronized關(guān)鍵字券躁,也可以在getSingleton方法內(nèi)增加sychronized來實現(xiàn)。
public class Singleton {
private static Singleton sSingleton = null;
//限制產(chǎn)生多個對象
private Singleton() {
}
//通過該方法獲得實例對象
public static sychronized Singleton getSingleton() {
if (sSingleton == null) {
sSingleton = new Singleton();
}
return sSingleton;
}
}
優(yōu)點是單例只有在使用時才會被實例化掉盅,在一定程序上節(jié)約了資源也拜;缺點是第一次加載時需要及時進行實例化,反應(yīng)稍慢趾痘,最大的問題是每次調(diào)用getInstance都進行同步慢哈,造成不必要的同步開銷。
所以相比懶漢式永票,更加推薦餓漢式卵贱,當(dāng)然各有利弊滥沫,下文會推薦幾種適用的單例模式,別著急键俱,接著往下看兰绣。
9. 注意事項(二)
除了擔(dān)心高并發(fā)以外,還需要考慮對象的復(fù)制情況编振。
在Java中缀辩,對象默認是不可以被復(fù)制的,若實現(xiàn)Cloneable接口踪央,實現(xiàn)clone方法臀玄,則可以直接通過對象復(fù)制方法創(chuàng)建一個新對象,對象復(fù)制是不用調(diào)用類的構(gòu)造函數(shù)的畅蹂,因此即使是私有的構(gòu)造函數(shù)健无,對象仍然可以被復(fù)制。所以解決該問題的方法就是單例類不要實現(xiàn)Cloneable接口魁莉。
10. 推薦寫法
- 餓漢式模式
public class Singleton {
private static final Singleton sSingleton = new Singleton();
//限制產(chǎn)生多個對象
private Singleton() {
}
//通過該方法獲得實例對象
public static Singleton getSingleton() {
return sSingleton;
}
//類中其他方法睬涧,盡量是static
public static void doSometing() {
}
}
優(yōu)點:類加載時就完成了初始化,所以類加載較慢旗唁,但獲取對象的速度快畦浓。這種方式基于類加載機制,避免了多線程的同步問題检疫。
缺點:不能達到懶加載的效果讶请,如果從始至終未使用過這個實例,則會造成內(nèi)存的浪費屎媳。
推薦場景:單例模式經(jīng)常使用的場景下夺溢,選擇餓漢式。
- 雙重檢索模式(DCL)
public class Singleton {
private static Singleton sSingleton = null;
//限制產(chǎn)生多個對象
private Singleton() {
}
//通過該方法獲得實例對象
public static Singleton getSingleton() {
if (sSingleton == null) {
synchronized (Singleton.class) {
if (sSingleton == null) {
sSingleton = new Singleton();
}
}
}
return sSingleton;
}
}
亮點:在getSingleton方法中對instance進行了兩次判空:第一層判斷主要是為了避免不必要的同步烛谊,第二層判斷則是為了在Singleton等于null的情況下才創(chuàng)建實例风响。
分析:
sSingleton = new Singleton()這句話大致做了3件事情:
①:給Singleton的實例分配內(nèi)存。
②:調(diào)用Singleton()構(gòu)造函數(shù)丹禀,初始化成員字段状勤。
③:將sSingleton對象指向分配的內(nèi)存空間(此時sSingleton不是null了)
但是由于Java編譯器允許處理器亂序執(zhí)行等原因,執(zhí)行順序可能是1,2,3双泪,還可能是1,3,2.如果是后者持搜,并且在3執(zhí)行完畢、2未執(zhí)行之前焙矛,被切換到線程B上葫盼,這時候sSingleton因為已經(jīng)在線程A內(nèi)執(zhí)行過了第三點,sSingleton已經(jīng)是非空了村斟,所以線程B直接取走sSingleton贫导,再使用時就會出錯抛猫,導(dǎo)致DCL失效。
為了避免這類事情的發(fā)生脱盲,JDK1.5之后調(diào)整了JVM,具體化了volatile關(guān)鍵字邑滨,只需要將sSingleton定義改成private volatile static Singleton sSingleton = null 就可以保證sSingleton對象每次都是從主內(nèi)存中讀取的,保證了可見性和有序性钱反,這樣的話就可以使用DCL掖看。以后會有相關(guān)Java內(nèi)存方面的文章,具體闡述volatile關(guān)鍵字面哥。
優(yōu)點:資源利用率高哎壳,第一次執(zhí)行g(shù)etSingleton時單例對象才會被實例化。
缺點:第一次加載時反應(yīng)稍慢尚卫。
推薦理由:資源利用率高归榕!線程安全!絕大多數(shù)場景下可以保證單例對象的唯一性吱涉。
完整代碼:
public class Singleton {
private volatile static Singleton sSingleton = null;
//限制產(chǎn)生多個對象
private Singleton() {
}
//通過該方法獲得實例對象
public static Singleton getSingleton() {
if (sSingleton == null) {
synchronized (Singleton.class) {
if (sSingleton == null) {
sSingleton = new Singleton();
}
}
}
return sSingleton;
}
}
- 靜態(tài)內(nèi)部類單例模式(最推薦的)
DCL雖然解決了資源消耗刹泄、多余的同步、線程安全等問題怎爵,但是在高并發(fā)場景比較復(fù)雜的情況下依然會出現(xiàn)失效的問題特石,所以推薦使用靜態(tài)內(nèi)部類單例模式:
public class Singleton {
//限制產(chǎn)生多個對象
private Singleton() {
}
//通過該方法獲得實例對象
public static Singleton getInstance() {
return SingletonHolder.sInstance;
}
/**
* 靜態(tài)內(nèi)部類
*/
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
優(yōu)點:
- 懶加載:
第一次加載Singleton類時并不會初始化sInstance,只有在第一次調(diào)用Singleton的getInstance方法時才會導(dǎo)致sInstance被初始化鳖链。 - 線程安全和單例對象唯一性:
第一次調(diào)用Singleton的getInstance方法時會導(dǎo)致虛擬機加載SingletonHolder類姆蘸,這種方式確保了線程安全,還保證了單例對象的唯一性芙委。
推薦理由:懶加載逞敷、線程安全、單例對象唯一性灌侣。
- 枚舉模式
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("do sth.");
}
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
優(yōu)點:默認枚舉實例的創(chuàng)建實線程安全的推捐,并且在任何情況下它都是一個單例,包括序列化侧啼。
推薦理由:寫法簡單玖姑,甚至在反序列化的情況下,依然可以保證單例的唯一性慨菱。
- 容器模式
public class Singleton {
private static Map<String, Object> objMap = new HashMap<String, Object>();
private Singleton() {
}
public static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
return objMap.get(key);
}
}
分析:在程序的初始,將多種單例類型注入到一個統(tǒng)一的管理類中戴甩,在使用時根據(jù)key獲取對象對應(yīng)類型的對象符喝。這種方式使得我們可以管理多種類型的單例,并且在使用時可以通過統(tǒng)一的接口進行獲取操作甜孤,降低了用戶的使用成本协饲,也對用戶隱藏了具體實現(xiàn)畏腕,降低了耦合度。
11. 特別注意
通過反序列化茉稠,上面幾種單例模式都會出現(xiàn)
重新創(chuàng)建對象的情況描馅,枚舉不包括在內(nèi)。
通過反序列化可以將一個單例的實例對象寫到磁盤而线,然后再讀回來铭污,從而獲得一個實例。及時構(gòu)造函數(shù)是私有的膀篮,反序列化依然可以通過特殊途徑去創(chuàng)建類的一個新的實例嘹狞。
反序列化操作提供了一個很特別的函數(shù),類中具有一個私有的誓竿、被實例化的方法readResolve磅网,通過這個方法可以讓開發(fā)人員控制對象的反序列化。
所以上述幾個示例(不包括枚舉)中如果要杜絕單例對象在被反序列化時重新生成對象筷屡,必須加入以下方法
private Object readResolve() throws ObjectStreamException {
return sInstance;
}
readResolve方法將sInstance對象返回涧偷,而不是默認的重新生成一個新的對象。
12. 后續(xù)
如果大家喜歡這篇文章毙死,歡迎點贊燎潮;如果想看更多 設(shè)計模式 方面的技術(shù),歡迎關(guān)注规哲!