【Java】單例模式的幾種寫法

應(yīng)用最廣的模式——單例模式

一、單例模式的定義和關(guān)鍵點(diǎn)

  • 定義
    確保一個(gè)類只有一個(gè)實(shí)例對象,而且自行實(shí)例化向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。
  • 實(shí)現(xiàn)關(guān)鍵點(diǎn)
    私有的構(gòu)造器,不能對外開放;
    通過靜態(tài)方法或者枚舉返回一個(gè)單例類對象;
    確保單例類對象有且僅有一個(gè),尤其是在多線程環(huán)境下;
    確保單例類對象在反序列化是不會(huì)重新構(gòu)建對象;

二、UML類圖和應(yīng)用場景

  • 應(yīng)用場景

    許多時(shí)候整個(gè)系統(tǒng)只需要一個(gè)全局對象,方便我們協(xié)調(diào)整個(gè)系統(tǒng)的行為。比如我們經(jīng)用用于IO、數(shù)據(jù)庫、Log管理等工具類,都可以考慮使用單例模式,這樣避免產(chǎn)生多個(gè)對象耗費(fèi)系統(tǒng)資源。


    圖2-1

三、單例模式的幾種寫法

① 餓漢單例

public class Hungry{
    //餓漢單例
    private static Hungry instance = new Hungry();
    private Hungry() {
    }
    public static synchronized Hungry getInstance() {
        return instance;
    }
}

?注:該方式每次調(diào)用getInstance()都需要進(jìn)行同步,造成不必要的開銷,所以不推薦使用

② 懶漢單例

public class Idler{
    //懶漢單例
    private static Idler instace;
    private Idler() {
    }
    public static synchronized Idler getInstance() {
        if(instace == null) {
            instace = new Idler();
        }
        return instace;
    }
}

?注:情況跟餓漢單例差不多,同樣不推薦使用。

③ DCL單例(雙重檢查)

public class DCL{
    //Jdk5之后加入volatitle保證每次都從主內(nèi)存取出
    private static volatile DCL sInstance = null;
    private DCL() {
    }
    public static DCL getInstance() {
        if(sInstance == null ) {
            //既要檢查是否為空對象
            //又要檢查是否線程安全
            synchronized(DCL.class) {
                if(sInstance == null ) {
                    sInstance = new DCL();
                }
            }
        }
        return sInstance;
    }
}

?注:該方式在JDK5之前存在DCL失效問題,怎么失效呢?一般我們認(rèn)為編譯器會(huì)按以下順序編譯??

  1. 給DCL的實(shí)例分配內(nèi)存
  2. 調(diào)用DCL的構(gòu)造器,初始化成員變量
  3. 將sInstance引用指向分配的內(nèi)存空間(此時(shí)sInstance不為null)
    也就是說2和3 的無法保證順序執(zhí)行,它可能會(huì)執(zhí)行 1->3->2, 這樣的話 線程A已經(jīng)執(zhí)行過了3,但未執(zhí)行2,就切換到線程B,再使用sInstance 就會(huì)出錯(cuò)。
    總體來說 JDK5之后,SUN修復(fù)這個(gè)問題,只需將sInstance加入volatile就可以保證每次從主內(nèi)存讀取,這樣或多或少會(huì)影響到一些性能,所以該方式在高并發(fā)的環(huán)境下(高于JDK5版本)一般能滿足需求。

④ 靜態(tài)內(nèi)部單例

public class StaticInner{
     //靜態(tài)內(nèi)部類單例
    private StaticInner() {
    }
    private static class  StaticInnerHolder{
        private static final StaticInner inner = new StaticInner();
    }
    public static StaticInner getInstance() {
        return StaticInnerHolder.inner;
    }
}

?注:該方式只有在第一次調(diào)用getInstance()方法才會(huì)使虛擬機(jī)加載StaticInnerHolder類,這樣不僅能保證線程安全,還能保證單例對象的唯一性, 所以推薦使用 。

⑤ 枚舉單例

//枚舉單例
public enum SingletonEum{
    INSTANCE;
    public void doSomething() {
        //各種方法實(shí)現(xiàn)
    }
}

?注:這種方式的特點(diǎn)就是簡單,重要的是默認(rèn)枚舉類型的創(chuàng)建是線程安全的,所以《Effective Java》的作者認(rèn)為“單元素的枚舉類型已經(jīng)成為單例模式的最佳方法”,但是枚舉類型在Android的使用也是比較占用內(nèi)存的(一次面試官提到)還需要謹(jǐn)慎使用,我是一臉懵逼的 ??

[2019-8-17]關(guān)于枚舉占用Android 內(nèi)存問題討論:https://www.liaohuqiu.net/cn/posts/android-enum-memory-usage/

以上幾種實(shí)現(xiàn)方式,為了防止反序化時(shí)重新創(chuàng)建對象的情況,還需進(jìn)一步處理,加入readResolve函數(shù)。例如:

public class Hungry implements Serializable{
 //餓漢單例 
 .........
  private Object readResolve() throw ObjectStreamException{
      return instance;
  }
}

四、總結(jié)

實(shí)現(xiàn)方式 優(yōu)缺點(diǎn) 說點(diǎn)什么
餓漢&懶漢單例 只有使用的才會(huì)被實(shí)例化,但是需要同步開銷 不推薦
DCL單例 一定程度上解決資源消耗、多余同步線程安全問題,但是因?yàn)镴DK5之前的失效問題需要volatile的加入,影響些性能; 看著辦吧
靜態(tài)內(nèi)部類單例 能保證線程安全,又能保證單例對象唯一性 推薦使用
枚舉單例 簡單方便、線程安全,但Android枚舉類型占用內(nèi)存的憂慮 考慮一下

??僅學(xué)習(xí)記錄,勉勵(lì)成長;

參考文獻(xiàn):
[1]何紅輝·關(guān)愛民.Android設(shè)計(jì)模式與實(shí)站[J].第二版,2017:25-29.
[2]Joshua Bloch·楊春花.俞黎敏.Effective Java中文版第2版[J].第二版,2017:15.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容