Android 開發(fā)單例模式線程安全與序列化

前言

單例模式是最常用到的設(shè)計模式之一短条,熟悉設(shè)計模式的朋友對單例模式都不會陌生。一般介紹單例模式都只會提到餓漢式懶漢式這兩種實現(xiàn)方式。

看完本章后,你可能會發(fā)現(xiàn)項目中的并沒有正確的使用創(chuàng)建單例氯材,本文會將單例模式的創(chuàng)建方式和優(yōu)缺點詳細描述渣锦。

image

一、單例模式介紹

單例模式(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)主要是通過以下兩個步驟:

  1. 將該類的構(gòu)造方法定義為私有方法椒惨,這樣其他處的代碼就無法通過調(diào)用該類的構(gòu)造方法來實例化該類的對象,只有通過該類提供的靜態(tài)方法來得到該類的唯一實例潮罪;
  2. 在該類內(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)鍵字作用

  1. 防止指令重排:規(guī)定了 volatile 變量不能指令重排线欲,必須先寫再讀。

  2. 內(nèi)存可見:線程從內(nèi)存中讀取 volatile 修飾的變量的數(shù)據(jù)俄精,直接從主內(nèi)存中獲取數(shù)據(jù)询筏,不需要經(jīng)過 CPU 緩存,這樣使得多線程獲取的數(shù)據(jù)都是一致的竖慧。如圖所示:

  • volatile 和 synchronized 區(qū)別

volatile 不能夠替代 synchronized嫌套,原因有兩點:

  1. 對于多線程,不是一種互斥關(guān)系
  2. 不能保證變量狀態(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í)價值洲赵,如果有疑問大家可以留言交流,謝謝大家對的支持。


111111111111111.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末叠萍,一起剝皮案震驚了整個濱河市芝发,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌苛谷,老刑警劉巖辅鲸,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異抄腔,居然都是意外死亡瓢湃,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門赫蛇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绵患,“玉大人,你說我怎么就攤上這事悟耘÷潋” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵暂幼,是天一觀的道長筏勒。 經(jīng)常有香客問我,道長旺嬉,這世上最難降的妖魔是什么管行? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮邪媳,結(jié)果婚禮上捐顷,老公的妹妹穿的比我還像新娘。我一直安慰自己雨效,他們只是感情好迅涮,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著徽龟,像睡著了一般叮姑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上据悔,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天传透,我揣著相機與錄音,去河邊找鬼屠尊。 笑死旷祸,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的讼昆。 我是一名探鬼主播托享,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼骚烧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了闰围?” 一聲冷哼從身側(cè)響起赃绊,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎羡榴,沒想到半個月后碧查,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡校仑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年忠售,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迄沫。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡稻扬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出羊瘩,到底是詐尸還是另有隱情泰佳,我是刑警寧澤,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布尘吗,位于F島的核電站逝她,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏睬捶。R本人自食惡果不足惜黔宛,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望擒贸。 院中可真熱鬧宁昭,春花似錦、人聲如沸酗宋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蜕猫。三九已至,卻和暖如春哎迄,著一層夾襖步出監(jiān)牢的瞬間回右,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工漱挚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留翔烁,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓旨涝,卻偏偏與公主長得像蹬屹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355