溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

深入淺析Java設(shè)計(jì)模式中的單例模式

發(fā)布時(shí)間:2020-11-11 17:10:45 來源:億速云 閱讀:237 作者:Leah 欄目:編程語言

這篇文章給大家介紹深入淺析Java設(shè)計(jì)模式中的單例模式,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。

單例模式是非常常見的設(shè)計(jì)模式,其含義也很簡單,一個(gè)類給外部提供一個(gè)唯一的實(shí)例。下文所有的代碼均在github

源碼整個(gè)項(xiàng)目不僅僅有設(shè)計(jì)模式,還有其他JavaSE知識(shí)點(diǎn),歡迎Star,F(xiàn)ork

單例模式的UML圖

深入淺析Java設(shè)計(jì)模式中的單例模式

單例模式的關(guān)鍵點(diǎn)

通過上面的UML圖,我們可以看出單例模式的特點(diǎn)如下:

1、構(gòu)造器是私有的,不允許外部的類調(diào)用構(gòu)造器
2、提供一個(gè)供外部訪問的方法,該方法返回單例類的實(shí)例

如何實(shí)現(xiàn)單例模式

上面已經(jīng)給出了單例模式的關(guān)鍵點(diǎn),我們的實(shí)現(xiàn)只需要滿足上面2點(diǎn)即可。但是正因?yàn)閱卫J降膶?shí)現(xiàn)方式比較寬松,所以不同的實(shí)現(xiàn)方式會(huì)有不同的問題。我們可以對(duì)單例模式的實(shí)現(xiàn)做一下分類,看一看有哪些不同的實(shí)現(xiàn)方式。

1根據(jù)單例對(duì)象的創(chuàng)建時(shí)機(jī)不同,可以分為餓漢模式和懶漢模式。餓漢是指在類加載的時(shí)候,就創(chuàng)建了對(duì)象。但是創(chuàng)建對(duì)象有時(shí)比較消耗資源,會(huì)造成類加載很慢,但是優(yōu)點(diǎn)是獲取對(duì)象的速度很快,因?yàn)樵缫呀?jīng)創(chuàng)建好了嘛。懶漢就是相對(duì)餓漢而言,在需要返回單例對(duì)象的時(shí)候,在創(chuàng)建對(duì)象,類加載的時(shí)候,并不初始化,好處與缺點(diǎn)也不言而喻

2.根據(jù)是否實(shí)現(xiàn)線程安全,可以分為普通的懶漢模式這種線程不安全的寫法,和餓漢模式,雙重檢查鎖的懶漢模式,以及通過靜態(tài)內(nèi)部類或者枚舉類等實(shí)現(xiàn)的線程安全的寫法。

一個(gè)線程不安全的單例模式

public class SimpleSingleton {

  private static SimpleSingleton simpleSingleton;

  private SimpleSingleton(){

  }

  public static SimpleSingleton getInstance(){
    if (simpleSingleton == null) {
      simpleSingleton = new SimpleSingleton();
    }
    return simpleSingleton;
  }
}

首先,我們可以看出這是一個(gè)懶漢模式的實(shí)現(xiàn)。因?yàn)橹挥性趃etInstance的時(shí)候,才會(huì)真正創(chuàng)建單例的對(duì)象。但是為什么他是線程不安全的呢,是因?yàn)榭赡軙?huì)有2個(gè)線程同時(shí)進(jìn)入if (simpleSingleton == null)的判斷,就是同時(shí)創(chuàng)建了simpleSingleton對(duì)象。

DCL懶漢模式

上面的方法可以看出是存在線程不安全的問題的,我們可以用同步關(guān)鍵字synchronized來實(shí)現(xiàn)線程安全。我們先逐步分析,先用synchronized來改寫上面的懶漢模式,代碼如下:

public class DCLSingleton {

  private static DCLSingleton singleton;
  private DCLSingleton(){
  }

  public synchronized static DClSingleton getSingleton(){
    if (singleton == null) {
      singleton = new DCLSingleton();
    }
    return singleton;
  }

}

這樣,就有效的保證了不會(huì)有兩個(gè)線程同時(shí)執(zhí)行該方法,但這個(gè)效率也太低了吧。因?yàn)樵趧?chuàng)建實(shí)例之后,每次得到實(shí)例對(duì)象,還是需要進(jìn)行同步,synchronized的同步保證代價(jià)是比較大的,因此可以在此基礎(chǔ)上進(jìn)行改造。在已經(jīng)創(chuàng)建好之后,就不需要同步了,我們可以改成如下的形式:

  public static DCLSingleton getSingleton(){
    if (singleton == null) {
      synchronized (DCLSingleton.class) {
        if (singleton == null) {
          singleton = new DCLSingleton();
        }
      }
    }
    return singleton;
  }

其他代碼不變,只看這個(gè)方法。該方法的兩重if (singleton == null)可以有效地保證線程安全。比如,當(dāng)兩個(gè)線程同時(shí)進(jìn)入該方法的時(shí)候,第一個(gè)if,兩者都是進(jìn)入,下面的代碼,但是碰到同步代碼塊,只能有一個(gè)先進(jìn)入,進(jìn)入的時(shí)候,繼續(xù)判斷,再次判斷為空,才會(huì)真正創(chuàng)建對(duì)象。如果不進(jìn)行,第二個(gè)判斷,那些對(duì)于第一個(gè)進(jìn)入的線程而言,確實(shí)創(chuàng)建了對(duì)象,但是第二個(gè)線程,他緊接著也會(huì)執(zhí)行創(chuàng)建對(duì)象的操作,因?yàn)椴恢赖谝粋€(gè)線程已經(jīng)創(chuàng)建成功。因此,需要兩次判空。
但是真的就如此簡單的保證了線程安全嗎?我們仔細(xì)分析一下這個(gè)過程,singleton = new DCLSingleton();這個(gè)代碼實(shí)際上是3個(gè)操作。

1.給DCLSingleton實(shí)例分配內(nèi)存
2.調(diào)用DCLSingleton()的構(gòu)造函數(shù),初始化成員字段
3.將singleton對(duì)象指向分配的內(nèi)存空間。

在JDK1.5以前,上面的3個(gè)執(zhí)行順序是不固定的,有可能是1-2-3,或者1-3-2。如果是1-3-2,則在第一個(gè)線程執(zhí)行完第三步以后,第二個(gè)線程立即執(zhí)行,但還沒有真正的進(jìn)行初始化,所以就會(huì)使用的時(shí)候出錯(cuò)。在JDK1.5以后,我們可以用volatile關(guān)鍵字來保證該1-2-3的順序執(zhí)行。所以,除了getSingleton()方法要改成上面的樣子以外,還需要對(duì)private static DCLSingleton singleton; 改寫成private static volatile DCLSingleton singleton; 這樣,就真正保證了線程同步的懶漢寫法的單例模式。

餓漢寫法

餓漢寫法有很多變形,但無論是哪一種變形,都能保證線程安全,因?yàn)轲I漢寫法是在類加載的時(shí)候,就完成了對(duì)象的初始化,類加載保證了他們天生是線程安全的。下面給出常見的2中餓漢寫法

public class HungrySingleton {
  private static final HungrySingleton singleton = new HungrySingleton();

  private HungrySingleton(){

  }

  public static HungrySingleton getSingleton(){
    return singleton;
  }
}
public class HungrySingleton {
  private static final HungrySingleton singleton = new HungrySingleton();

  private HungrySingleton(){

  }

//  public static HungrySingleton getSingleton(){
//    return singleton;
//  }
}

這兩種對(duì)初始化單例的對(duì)象上面,都是一致的, 通過final來保證對(duì)象的唯一。不同的是,調(diào)用單例對(duì)象的方式,第一種是通過getSingleton(),第二種是通過類.類變量的形式。

靜態(tài)內(nèi)部類實(shí)現(xiàn)單例模式

雙重檢查鎖(DCL)實(shí)現(xiàn)單例模式,雖然解決了線程不安全的問題,以及保證了資源的懶加載,在需要的時(shí)候,才會(huì)進(jìn)行實(shí)例化的操作。但是在某些情況下(比如JDK低于1.5)會(huì)出現(xiàn)DCL失效,所以有一種很簡潔且依舊是懶加載的方法實(shí)現(xiàn)單例模式。寫法如下:

public class StaticSingleton {

  private StaticSingleton(){
  }
  public static final StaticSingleton getInstance(){
    return Holder.singleton;
  }

  private static class Holder{
    private static final StaticSingleton singleton = new StaticSingleton();
  }
}

通過靜態(tài)內(nèi)部類的形式,實(shí)現(xiàn)單例類的初始化,其特性同樣是通過ClassLoader來保證其單例對(duì)象的唯一,但是這是懶加載的,因?yàn)橹挥性贖older類被調(diào)用的時(shí)候,即getInstance調(diào)用的時(shí)候,才會(huì)加載Holder類從而實(shí)現(xiàn)創(chuàng)建對(duì)象。

枚舉類實(shí)現(xiàn)單例模式

直接看代碼:

public enum EnumSingleton {
  SINGLETON;
  public void doSometings(){
    
  }
}

使用的時(shí)候,直接通過EnumSingleton.SINGLETON.doSomethings()。枚舉類天生特性是保證不會(huì)有兩個(gè)實(shí)例,并且只有在第一次訪問的時(shí)候才會(huì)被實(shí)例化,是懶加載的情況。

真的不會(huì)再次創(chuàng)建新的對(duì)象嗎?

在常規(guī)調(diào)用單例類的getInstance()方法的情況下,使用線程安全的寫法確實(shí)不會(huì)創(chuàng)建新的對(duì)象,但是Java提供了很多奇特的技巧和使用,下面這些使用會(huì)破壞掉常規(guī)的單例。

  • 反序列化
  • 反射
  • 克隆
  • 分布式環(huán)境下,多個(gè)類加載器
     

在除了枚舉實(shí)現(xiàn)單例模式的方法以外,其余所有方法碰到上述四種情況,都會(huì)重新創(chuàng)建對(duì)象。原因如下:

  • 反序列化會(huì)調(diào)用一個(gè)特殊的readResolve()方法來創(chuàng)建新的對(duì)象。我們可以重寫該方法,讓他返回原來的instance,而不是重新創(chuàng)建一個(gè)。
  • 反射會(huì)得到私有的構(gòu)造函數(shù),只能在構(gòu)造函數(shù)中加一個(gè)判斷,如果對(duì)象不為null,則扔出一個(gè)運(yùn)行時(shí)異常,如果不這樣,只有枚舉能解決,因?yàn)槊杜e自帶的特性。
  • 克隆,因?yàn)橹苯涌截惖膬?nèi)存空間的內(nèi)容,所以只有自己重寫單例類的clone方法,如果不這樣,也只有枚舉能解決,因?yàn)槊杜e沒有克隆方法。
  • 多分布式環(huán)境,因?yàn)槲覀兩鲜龊芏喾N單例的寫法,都是依賴于類加載器的特性,但是static的作用只負(fù)責(zé)到類加載器,所以當(dāng)工程中存在多個(gè)類加載器的時(shí)候,就會(huì)創(chuàng)建多個(gè)實(shí)例,這種通常就需要第三方庫來解決。
     

什么時(shí)候用單例模式,用哪一種寫法的單例模式

單例模式有兩種比較適合的使用場(chǎng)景。
第一種是創(chuàng)建某個(gè)對(duì)象,需要的代價(jià)比較大,為了避免頻繁的創(chuàng)建和銷毀對(duì)象從而引起的對(duì)資源的浪費(fèi),會(huì)考慮使用單例模式。
第二種是這個(gè)對(duì)象必須只有一個(gè),有多個(gè)會(huì)造成不可預(yù)估的錯(cuò)誤,或者程序的混亂,比如只會(huì)有一個(gè)序號(hào)生成器,一個(gè)緩存等等。
針對(duì)使用的單例模式,如果需要理解的加載資源,就是用餓漢寫法,在Android應(yīng)用中,很多對(duì)象需要在啟動(dòng)的時(shí)候,立即就使用,比如啟動(dòng)時(shí),需要拉取相機(jī)配置的類管理縮略圖的cache類等等。如果不是立即需要,或者不是貫穿應(yīng)用始終的,就不需要使用餓漢寫法,可以考慮懶漢寫法用(DCL或者靜態(tài)內(nèi)部類實(shí)現(xiàn))這兩種在一般情況下都不會(huì)出現(xiàn)問題。

關(guān)于深入淺析Java設(shè)計(jì)模式中的單例模式就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI