前言
單例模式是最常用到的設(shè)計模式之一短条,熟悉設(shè)計模式的朋友對單例模式都不會陌生。一般介紹單例模式都只會提到餓漢式和懶漢式這兩種實現(xiàn)方式。
看完本章后,你可能會發(fā)現(xiàn)項目中的并沒有正確的使用創(chuàng)建單例氯材,本文會將單例模式的創(chuàng)建方式和優(yōu)缺點詳細描述渣锦。
一、單例模式介紹
單例模式(Singleton Pattern)是 Java 中最簡單的設(shè)計模式之一浓体。這種類型的設(shè)計模式屬于創(chuàng)建型模式泡挺,它提供了一種創(chuàng)建對象的最佳方式。
這種模式涉及到一個單一的類命浴,該類負責(zé)創(chuàng)建自己的對象,同時確保只有單個對象被創(chuàng)建贱除。這個類提供了一種訪問其唯一的對象的方式生闲,可以直接訪問,不需要實例化該類的對象月幌。
很多時候整個系統(tǒng)只需要擁有一個的全局對象碍讯,這樣有利于我們協(xié)調(diào)系統(tǒng)整體的行為。比如在某個服務(wù)器程序中扯躺,該服務(wù)器的配置信息存放在一個文件中捉兴,這些配置數(shù)據(jù)由一個單例對象統(tǒng)一讀取,然后服務(wù)進程中的其他對象再通過這個單例對象獲取這些配置信息录语。這種方式簡化了在復(fù)雜環(huán)境下的配置管理倍啥。
二、單例模式的特點
單例模式具備以下特點:
- 單例類只能有一個實例澎埠。
- 單例類必須自己創(chuàng)建自己的唯一實例虽缕。
- 單例類必須給所有其他對象提供這一實例。
優(yōu)點:由于單例模式只生成了一個實例蒲稳,所以能夠節(jié)約系統(tǒng)資源氮趋,減少性能開銷,提高系統(tǒng)效率江耀,同時也能夠嚴格控制客戶對它的訪問剩胁。
缺點:也正是因為系統(tǒng)中只有一個實例,這樣就導(dǎo)致了單例類的職責(zé)過重祥国,違背了“單一職責(zé)原則”昵观,同時也沒有抽象類,這樣擴展起來有一定的困難系宫。
三索昂、單例模式的實現(xiàn)
單例模式要求類能夠有返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態(tài)方法,通常使用 getInstance 這個名稱)扩借。
單例的實現(xiàn)主要是通過以下兩個步驟:
- 將該類的構(gòu)造方法定義為私有方法椒惨,這樣其他處的代碼就無法通過調(diào)用該類的構(gòu)造方法來實例化該類的對象,只有通過該類提供的靜態(tài)方法來得到該類的唯一實例潮罪;
- 在該類內(nèi)提供一個靜態(tài)方法康谆,當(dāng)我們調(diào)用這個方法時领斥,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創(chuàng)建該類的實例并將實例的引用賦予該類保持的引用沃暗。
1月洛、餓漢模式(線程安全)
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
- 優(yōu)點:這種寫法比較簡單,就是在類裝載的時候就完成實例化孽锥。避免了線程同步問題嚼黔。
- 缺點:在類裝載的時候就完成實例化,沒有達到 Lazy Loading 的效果惜辑。如果從始至終從未使用過這個實例唬涧,則會造成內(nèi)存的浪費。
2盛撑、懶漢模式(線程不安全)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
這種寫法起到了 Lazy Loading 的效果碎节,但是只能在單線程下使用。如果在多線程下抵卫,一個線程進入了 if (singleton == null)判斷語句塊狮荔,還未來得及往下執(zhí)行,另一個線程也通過了這個判斷語句介粘,這時便會產(chǎn)生多個實例殖氏。所以在多線程環(huán)境下不可使用這種方式。
3碗短、懶漢式(線程安全)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
解決線程不安全問題受葛,做個線程同步就可以了,于是就對 getInstance()方法進行了線程同步偎谁。
缺點:效率太低了总滩,每個線程在想獲得類的實例時候,執(zhí)行 getInstance()方法都要進行同步巡雨。而其實這個方法只執(zhí)行一次實例化代碼就夠了闰渔,后面的想獲得該類實例,直接 return 就行了铐望。方法進行同步效率太低要改進冈涧。
4、靜態(tài)內(nèi)部類(線程安全)
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
這種方式跟餓漢式方式采用的機制類似正蛙,但又有不同督弓。兩者都是采用了類裝載的機制來保證初始化實例時只有一個線程。不同的地方在餓漢式方式是只要 Singleton 類被裝載就會實例化乒验,沒有 Lazy-Loading 的作用愚隧,而靜態(tài)內(nèi)部類方式在 Singleton 類被裝載時并不會立即實例化,而是在需要實例化時锻全,調(diào)用 getInstance 方法狂塘,才會裝載 SingletonInstance 類录煤,從而完成 Singleton 的實例化。
優(yōu)點:避免了線程不安全荞胡,延遲加載妈踊,效率高。
5泪漂、枚舉(線程安全)
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
借助 JDK1.5 中添加的枚舉來實現(xiàn)單例模式廊营。不僅能避免多線程同步問題,而且還能防止反序列化重新創(chuàng)建新的對象窖梁∽阜纾可能是因為枚舉在 JDK1.5 中才添加,所以在實際項目開發(fā)中纵刘,很少見人這么寫過。
6荸哟、雙重校驗鎖法(線程安全)
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
Double-Check 概念對于多線程開發(fā)者來說不會陌生假哎,如代碼中所示,我們進行了兩次 if (singleton == null)檢查鞍历,這樣就可以保證線程安全了舵抹。這樣,實例化代碼只用執(zhí)行一次劣砍,后面再次訪問時惧蛹,判斷 if (singleton == null),直接 return 實例化對象刑枝。
優(yōu)點:線程安全香嗓;延遲加載;效率較高装畅。
四靠娱、單例模式注意事項
1、內(nèi)存泄漏
單例模式在 Android 開發(fā)中會經(jīng)常用到掠兄,但是如果使用不當(dāng)就會導(dǎo)致內(nèi)存泄露像云。因為單例的靜態(tài)特性使得它的生命周期同應(yīng)用的生命周期一樣長,如果一個對象已經(jīng)沒有用處了蚂夕,但是單例還持有它的引用迅诬,那么在整個應(yīng)用程序的生命周期它都不能正常被回收,從而導(dǎo)致內(nèi)存泄露婿牍。
public class Singleton {
private static Singleton singleton = null;
private Context mContext;
public Singleton(Context mContext) {
this.mContext = mContext;
}
public static Singleton getSingleton(Context context){
if (null == singleton){
singleton = new Singleton(context);
}
return singleton;
}
}
調(diào)用 getInstance(Context context)方法的時候傳入的 context 參數(shù)是 Activity侈贷、Service 等上下文,就會導(dǎo)致內(nèi)存泄露牍汹。
當(dāng)我們退出 Activity 時铐维,該 Activity 就沒有用了柬泽,但是因為 singleton 作為靜態(tài)單例(在應(yīng)用程序的整個生命周期中存在)會繼續(xù)持有這個 Activity 的引用,導(dǎo)致這個 Activity 對象無法被回收釋放嫁蛇,這就造成了內(nèi)存泄露锨并。
為了避免這樣單例導(dǎo)致內(nèi)存泄露,我們可以將 context 參數(shù)改為全局的上下文:
public Singleton(Context mContext) {
this.mContext = mContext.getApplicationContext();
}
全局的上下文 Application Context 就是應(yīng)用程序的上下文睬棚,和單例的生命周期一樣長第煮,這樣就避免了內(nèi)存泄漏。單例模式對應(yīng)應(yīng)用程序的生命周期抑党,所以我們在構(gòu)造單例的時候盡量避免使用 Activity 的上下文包警,而是使用 Application 的上下文。
2底靠、線程安全
單例模式在多線程的應(yīng)用場合下必須小心使用害晦。如果當(dāng)唯一實例尚未創(chuàng)建時,有兩個線程同時調(diào)用創(chuàng)建方法暑中,那么它們同時沒有檢測到唯一實例的存在壹瘟,從而同時各自創(chuàng)建了一個實例,這樣就有兩個實例被構(gòu)造出來鳄逾,從而違反了單例模式中實例唯一的原則稻轨。解決這個問題的辦法是為指示類是否已經(jīng)實例化的變量提供一個互斥鎖(但是這樣會降低效率)。
- volatile 關(guān)鍵字
Volatile 變量具有 synchronized 的可見性特性雕凹,但是不具備原子特性殴俱。這就是說線程能夠自動發(fā)現(xiàn) volatile 變量的最新值。Volatile 變量可用于提供線程安全枚抵,但是只能應(yīng)用于非常有限的
- volatile 關(guān)鍵字作用
防止指令重排:規(guī)定了 volatile 變量不能指令重排线欲,必須先寫再讀。
內(nèi)存可見:線程從內(nèi)存中讀取 volatile 修飾的變量的數(shù)據(jù)俄精,直接從主內(nèi)存中獲取數(shù)據(jù)询筏,不需要經(jīng)過 CPU 緩存,這樣使得多線程獲取的數(shù)據(jù)都是一致的竖慧。如圖所示:
- volatile 和 synchronized 區(qū)別
volatile 不能夠替代 synchronized嫌套,原因有兩點:
- 對于多線程,不是一種互斥關(guān)系
- 不能保證變量狀態(tài)的“原子性操作”圾旨,所以 volatile 不能保證原子性問題
3踱讨、序列化傳遞數(shù)據(jù)
我們期望單例模式可以保證只創(chuàng)建一個實例,而通過特殊手段創(chuàng)建出其他的實例砍的,就對單例模式造成了破壞痹筛。反序列化就會破壞單例模式。
單例實現(xiàn)了 serializable 接口,反序列化時會通過反射調(diào)用無參構(gòu)造方法創(chuàng)建一個新的實例帚稠,這時就要重寫 readResolve 方法規(guī)避序列化破壞單例谣旁,如下:
//防止序列化破壞單例模式
public Object readResolve() {
return SingletonHolder.INSTANCE;
}
而枚舉在序列化的時候僅是將枚舉對象的 name 屬性輸出到結(jié)果中,反序列化時通過 java.lang.Enum 的 valueOf 方法根據(jù) name 查找枚舉對象滋早。同時榄审,編譯器是不允許任何對這種序列化機制的定制的,因此禁用了 writeObject杆麸、readObject搁进、readObjectNoData、writeReplace 和 readResolve 等方法昔头。
也就是說饼问,枚舉的反序列化不是通過反射實現(xiàn)的,所以不會破壞單例模式揭斧。
原則上不允許用單例模式序列化傳遞數(shù)據(jù)莱革,如果一定要這么做,請考慮數(shù)據(jù)恢復(fù)現(xiàn)場讹开。
五驮吱、總結(jié)
一般來說,單例模式有五種寫法:懶漢萧吠、餓漢、雙重檢驗鎖桐筏、靜態(tài)內(nèi)部類纸型、枚舉。
開發(fā)過程中梅忌,一般情況下直接使用餓漢式就好了狰腌,如果明確要求要懶加載(lazy initialization)傾向于使用靜態(tài)內(nèi)部類。如果涉及到反序列化創(chuàng)建對象時會試著使用枚舉的方式來實現(xiàn)單例牧氮。
一個單例模式琼腔,涉及到的知識點包括了線程安全,類加載機制踱葛,枚舉實現(xiàn)原理丹莲,序列化,反射等多個知識點尸诽。即使是已經(jīng)學(xué)過甥材,仍然有回顧的價值。
以上就是這篇文章的全部內(nèi)容了性含,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值洲赵,如果有疑問大家可以留言交流,謝謝大家對的支持。