單例模式

需要搞清楚的疑問

  • 為什么要使用單例?
  • 單例存在哪些問題?
  • 單例與靜態(tài)類的區(qū)別?
  • 有何替代的解決方案?
  • 如何理解單例模式中的唯一性
  • 如何實現(xiàn)線程唯一的單例
  • 如何實現(xiàn)集群環(huán)境下的單例
  • 如何實現(xiàn)一個多例模式

為什么要使用單例?

  1. 定義
    一個類只允許創(chuàng)建一個對象,這個類就是就是一個單例類,這種設(shè)計模式就是單例設(shè)計模式

  2. 用處

    • 從業(yè)務(wù)概念上,有些數(shù)據(jù)在系統(tǒng)中只應(yīng)該保存一份,就比較適合設(shè)計為單例類。比如,系統(tǒng)的配置信息類
    • 使用單例解決資源訪問沖突的問題
  3. 實現(xiàn)

java

  • 餓漢式
  • 懶漢式
  • 雙重檢測
  • 靜態(tài)內(nèi)部類
  • 枚舉

php

<?php

class Singleton
{
    //私有屬性,用于保存實例
    private static $instance;

    //構(gòu)造方法私有化,防止外部創(chuàng)建實例
    private function __construct() {}

    //克隆方法私有化,防止復(fù)制實例
    private function __clone() {}

    //公有屬性,用于測試
    public $a;

    //公有方法,用于獲取實例
    public static function getInstance()
    {
        // 判斷實例有無創(chuàng)建,沒有的話創(chuàng)建實例并返回,有的話直接返回
        if (!(self::$instance instanceof self)) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

單例存在哪些問題?

  1. 單例對 OOP 特性的支持不友好
    一旦你選擇將某個類設(shè)計成到單例類,也就意味著放棄了繼承和多態(tài)這兩個強有力的面向?qū)ο筇匦?,也就相?dāng)于損失了可以應(yīng)對未來需求變化的擴展性

  2. 單例會隱藏類之間的依賴關(guān)系
    代碼的可讀性非常重要。在閱讀代碼的時候,我們希望一眼就能看出類與類之間的依賴關(guān)系,搞清楚這個類依賴了哪些外部類。通過構(gòu)造函數(shù)、參數(shù)傳遞等方式聲明的類之間的依賴關(guān)系,我們通過查看函數(shù)的定義,就能很容易識別出來。但是,單例類不需要顯示創(chuàng)建、不需要依賴參數(shù)傳遞,在函數(shù)中直接調(diào)用就可以了。如果代碼比較復(fù)雜,這種調(diào)用關(guān)系就會非常隱蔽。在閱讀代碼的時候,我們就需要仔細查看每個函數(shù)的代碼實現(xiàn),才能知道這個類到底依賴了哪些單例類。

  3. 單例對代碼的擴展性不友好
    數(shù)據(jù)庫連接池來舉例解釋:
    在系統(tǒng)設(shè)計初期,我們覺得系統(tǒng)中只應(yīng)該有一個數(shù)據(jù)庫連接池,這樣能方便我們控制對數(shù)據(jù)庫連接資源的消耗。所以把數(shù)據(jù)庫連接池類設(shè)計成了單例類。
    但之后我們發(fā)現(xiàn),系統(tǒng)中有些 SQL 語句運行得非常慢。這些 SQL 語句在執(zhí)行的時候,長時間占用數(shù)據(jù)庫連接資源,導(dǎo)致其他 SQL 請求無法響應(yīng)。
    為了解決這個問題,我們希望將慢 SQL 與其他 SQL 隔離開來執(zhí)行。
    為了實現(xiàn)這樣的目的,我們可以在系統(tǒng)中創(chuàng)建兩個數(shù)據(jù)庫連接池,慢 SQL 獨享一個數(shù)據(jù)庫連接池,其他 SQL 獨享另外一個數(shù)據(jù)庫連接池,這樣就能避免慢 SQL 影響到其他 SQL 的執(zhí)行。
    如果我們將數(shù)據(jù)庫連接池設(shè)計成單例類,顯然就無法適應(yīng)這樣的需求變更,也就是說,單例類在某些情況下會影響代碼的擴展性、靈活性。
    所以,數(shù)據(jù)庫連接池、線程池這類的資源池,最好還是不要設(shè)計成單例類。實際上,一些開源的數(shù)據(jù)庫連接池、線程池也確實沒有設(shè)計成單例類。

  4. 單例對代碼的可測試性不友好

  5. 單例不支持有參數(shù)的構(gòu)造函數(shù)


單例與靜態(tài)類的區(qū)別?


有何替代的解決方案?

為了保證全局唯一,除了使用單例,我們還可以用靜態(tài)方法來實現(xiàn)。
不過,靜態(tài)方法這種實現(xiàn)思路,并不能解決我們之前提到的問題。
如果要完全解決這些問題,我們可能要從根上,尋找其他方式來實現(xiàn)全局唯一類了。比如,通過工廠模式、IOC 容器(比如 Spring IOC 容器)來保證,由程序員自己來保證(自己在編寫代碼的時候自己保證不要創(chuàng)建兩個類對象)。


如何理解單例模式中的唯一性?

作用范圍:進程(進程中唯一)

如何實現(xiàn)線程唯一的單例?

通過一個 HashMap 來存儲對象,其中 key 是線程 id,value 是對象

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);

    private static final ConcurrentHashMap<Long, IdGenerator> instances = new ConcurrentHashMap<>();

    private IdGenerator() {}

    public static IdGenerator getInstance() {
        Long currentThreadId = Thread.currentThread().getId();
        instances.putIfAbsent(currentThreadId, new IdGenerator());
        return instances.get(currentThreadId);
    }

    public long getId() {
        return id.incrementAndGet();
    }
}

如何實現(xiàn)集群環(huán)境下的單例?

集群相當(dāng)于多個進程構(gòu)成的一個集合,集群唯一就相當(dāng)于進程內(nèi)唯一,進程間也唯一

方法:

  • 把單例對象序列化并存儲到外部共享存儲區(qū)(比如文件)。進程在使用這個單例對象的時候,需要先從外部共享存儲區(qū)中將它讀取到內(nèi)存,并反序列化成對象,才能使用,使用完后還要再存儲回外部共享存儲區(qū)。
  • 為了保證任何時刻,進程間都只有一份對象存在,一個進程在獲取到對象后,需要對對象加鎖,避免其他進程獲取。在進程使用完這個對象后,還需顯示的將對象從內(nèi)存中刪除,并且釋放給對象加的鎖。

如何實現(xiàn)一個多例模式?

多例模式:一個類可以創(chuàng)建多個類,但是有個數(shù)限制,比如最多能創(chuàng)建3個。
實現(xiàn):通過一個 Map 來存儲對象類型和對象之間的對應(yīng)關(guān)系,來控制對象的個數(shù)


public class BackendServer {
  private long serverNo;
  private String serverAddress;

  private static final int SERVER_COUNT = 3;
  private static final Map<Long, BackendServer> serverInstances = new HashMap<>();

  static {
    serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));
    serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));
    serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));
  }

  private BackendServer(long serverNo, String serverAddress) {
    this.serverNo = serverNo;
    this.serverAddress = serverAddress;
  }

  public BackendServer getInstance(long serverNo) {
    return serverInstances.get(serverNo);
  }

  public BackendServer getRandomInstance() {
    Random r = new Random();
    int no = r.nextInt(SERVER_COUNT)+1;
    return serverInstances.get(no);
  }
}

對于多例模式,還有一種理解方式:同一類型的只能創(chuàng)建一個對象,不同類型的可以創(chuàng)建多個對象。這里的“類型”如何理解呢?
下面舉個例子,logger name 就是剛剛說的“類型”,同一個 logger name 獲取到的對象實例是相同的,不同的 logger name 獲取到的對象實例是不同的。


public class Logger {
  private static final ConcurrentHashMap<String, Logger> instances
          = new ConcurrentHashMap<>();

  private Logger() {}

  public static Logger getInstance(String loggerName) {
    instances.putIfAbsent(loggerName, new Logger());
    return instances.get(loggerName);
  }

  public void log() {
    //...
  }
}

//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");

這種多例模式的理解方式有點類似工廠模式。它跟工廠模式的不同之處是,多例模式創(chuàng)建的對象都是同一個類的對象,而工廠模式創(chuàng)建的是不同子類的對象

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

相關(guān)閱讀更多精彩內(nèi)容

  • 為什么說支持懶加載的雙重檢測不比餓漢式更優(yōu)? 單例設(shè)計模式(Singleton Design Pattern)理解...
    vannesspeng閱讀 280評論 0 0
  • 一、單例模式 一個類里只有一個實例,只有它本身可以調(diào)用 其它類想要調(diào)用,需要它提供一個全局訪問點 二、Pr...
    冷月_star閱讀 315評論 0 0
  • 單例模式:軟件設(shè)計模式,定義是單例對象的類只能允許一個實例存在。 1)properties讀取配置文件 ...
    呂游_b601閱讀 336評論 0 0
  • 單例模式:軟件設(shè)計模式,定義是單例對象的類只能允許一個實例存在。 1)properties讀取配置文件 ...
    白露為霜_l閱讀 102評論 0 0
  • 單例模式是設(shè)計模式中最簡單的形式之一。這一模式的目的是使得類的一個對象成為系統(tǒng)中的唯一實例。 * 餓漢模式 pub...
    洛_60e3閱讀 845評論 0 0

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