Android開發(fā)教程——設(shè)計模式之單例模式

這篇文章將解決你以下幾個疑問:

  • 為什么要使用單例?
  • 單例有哪些寫法掂林?
  • 單例存在哪些問題臣缀?
  • 單例與靜態(tài)類的區(qū)別?
  • 有何替代的解決方案泻帮?
  • 相關(guān)視頻教程
為什么要使用單例精置?

單例設(shè)計模式(Singleton Design Pattern)如果一個類只允許創(chuàng)建一個對象(或者實例),那這個類就是一個單例類刑顺,這種設(shè)計模式就叫作單例設(shè)計模式氯窍,簡稱單例模式饲常。

為什么我們需要單例這種設(shè)計模式蹲堂?它能解決哪些問題?接下來我通過兩個實戰(zhàn)案例來講解:

實戰(zhàn)案例一:處理資源訪問沖突

我們自定義實現(xiàn)了一個往文件中打印日志的 Logger 類贝淤。具體的代碼實現(xiàn)如下所示:

public class Logger {
  private FileWriter writer;
  
  public Logger() {
    File file = new File("/Users/mrHandson/log.txt");
    writer = new FileWriter(file, true); //true表示追加寫入
  }
  
  public void log(String message) {
    writer.write(message);
  }
}

// Logger類的應(yīng)用示例:
public class UserController {
  private Logger logger = new Logger();
  
  public void login(String username, String password) {
    // ...省略業(yè)務(wù)邏輯代碼...
    logger.log(username + " logined!");
  }
}

public class OrderController {
  private Logger logger = new Logger();
  
  public void create(OrderVo order) {
    // ...省略業(yè)務(wù)邏輯代碼...
    logger.log("Created an order: " + order.toString());
  }
}

所有的日志都寫入到同一個文件 /Users/wangzheng/log.txt 中柒竞。在 UserController 和 OrderController 中,我們分別創(chuàng)建兩個 Logger 對象播聪。在 Web 容器的 Servlet 多線程環(huán)境下朽基,如果兩個 Servlet 線程同時分別執(zhí)行 login() 和 create() 兩個函數(shù)布隔,并且同時寫日志到 log.txt 文件中,那就有可能存在日志信息互相覆蓋的情況稼虎。

為什么會出現(xiàn)互相覆蓋呢衅檀?我們可以這么類比著理解。在多線程環(huán)境下霎俩,如果兩個線程同時給同一個共享變量加 1哀军,因為共享變量是競爭資源,所以打却,共享變量最后的結(jié)果有可能并不是加了 2杉适,而是只加了 1。同理柳击,這里的 log.txt 文件也是競爭資源猿推,兩個線程同時往里面寫數(shù)據(jù),就有可能存在互相覆蓋的情況捌肴。

實戰(zhàn)案例二:表示全局唯一類

從業(yè)務(wù)概念上蹬叭,如果有些數(shù)據(jù)在系統(tǒng)中只應(yīng)保存一份,那就比較適合設(shè)計為單例類状知。

比如具垫,配置信息類。在系統(tǒng)中试幽,我們只有一個配置文件筝蚕,當配置文件被加載到內(nèi)存之后,以對象的形式存在铺坞,也理所應(yīng)當只有一份起宽。

如何實現(xiàn)一個單例?

要實現(xiàn)一個單例济榨,我們需要關(guān)注的點無外乎下面幾個: -構(gòu)造函數(shù)需要是 private 訪問權(quán)限的坯沪,這樣才能避免外部通過 new 創(chuàng)建實例;

  • 考慮對象創(chuàng)建時的線程安全問題擒滑;
  • 考慮是否支持延遲加載腐晾;
  • 考慮是否支持延遲加載;
  • 考慮 getInstance() 性能是否高(是否加鎖)丐一。
1. 餓漢式

餓漢式的實現(xiàn)方式比較簡單藻糖。在類加載的時候,instance 靜態(tài)實例就已經(jīng)創(chuàng)建并初始化好了库车,所以巨柒,instance 實例的創(chuàng)建過程是線程安全的。不過,這樣的實現(xiàn)方式不支持延遲加載(在真正用到 IdGenerator 的時候洋满,再創(chuàng)建實例)晶乔,從名字中我們也可以看出這一點。具體的代碼實現(xiàn)如下所示:

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

因為不支持延遲加載牺勾,如果實例占用資源多(比如占用內(nèi)存多)或初始化耗時長(比如需要加載各種配置文件)正罢,提前初始化實例是一種浪費資源的行為。最好的方法應(yīng)該在用到的時候再去初始化驻民。

如果實例占用資源多腺怯,按照 fail-fast 的設(shè)計原則(有問題及早暴露),那我們也希望在程序啟動時就將這個實例初始化好川无。如果資源不夠呛占,就會在程序啟動的時候觸發(fā)報錯(比如 Java 中的 PermGen Space OOM),我們可以立即去修復(fù)懦趋。這樣也能避免在程序運行一段時間后晾虑,突然因為初始化這個實例占用資源過多,導(dǎo)致系統(tǒng)崩潰仅叫,影響系統(tǒng)的可用性帜篇。

2. 懶漢式

懶漢式相對于餓漢式的優(yōu)勢是支持延遲加載。具體的代碼實現(xiàn)如下所示:

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  public static synchronized IdGenerator getInstance() {
    if (instance == null) {
      instance = new IdGenerator();
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

懶漢式的缺點也很明顯诫咱,我們給 getInstance() 這個方法加了一把大鎖(synchronzed)笙隙,導(dǎo)致這個函數(shù)的并發(fā)度很低。量化一下的話坎缭,并發(fā)度是 1竟痰,也就相當于串行操作了。而這個函數(shù)是在單例使用期間掏呼,一直會被調(diào)用坏快。如果這個單例類偶爾會被用到,那這種實現(xiàn)方式還可以接受憎夷。但是莽鸿,如果頻繁地用到,那頻繁加鎖拾给、釋放鎖及并發(fā)度低等問題祥得,會導(dǎo)致性能瓶頸,這種實現(xiàn)方式就不可取了蒋得。

3. 雙重檢測

餓漢式不支持延遲加載级及,懶漢式有性能問題,不支持高并發(fā)窄锅。那我們再來看一種既支持延遲加載创千、又支持高并發(fā)的單例實現(xiàn)方式缰雇,也就是雙重檢測實現(xiàn)方式入偷。

在這種實現(xiàn)方式中追驴,只要 instance 被創(chuàng)建之后,即便再調(diào)用 getInstance() 函數(shù)也不會再進入到加鎖邏輯中了疏之。所以殿雪,這種實現(xiàn)方式解決了懶漢式并發(fā)度低的問題。具體的代碼實現(xiàn)如下所示:

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    if (instance == null) {
      synchronized(IdGenerator.class) { // 此處為類級別的鎖
        if (instance == null) {
          instance = new IdGenerator();
        }
      }
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

有人說锋爪,這種實現(xiàn)方式有些問題丙曙。因為指令重排序,可能會導(dǎo)致 IdGenerator 對象被 new 出來其骄,并且賦值給 instance 之后亏镰,還沒來得及初始化(執(zhí)行構(gòu)造函數(shù)中的代碼邏輯),就被另一個線程使用了拯爽。要解決這個問題索抓,我們需要給 instance 成員變量加上 volatile 關(guān)鍵字,禁止指令重排序才行毯炮。實際上逼肯,只有很低版本的 Java 才會有這個問題。我們現(xiàn)在用的高版本的 Java 已經(jīng)在 JDK 內(nèi)部實現(xiàn)中解決了這個問題(解決的方法很簡單桃煎,只要把對象 new 操作和初始化操作設(shè)計為原子操作篮幢,就自然能禁止重排序)。關(guān)于這點的詳細解釋为迈,跟特定語言有關(guān)三椿,我就不展開講了,感興趣的同學(xué)可以自行研究一下葫辐。

4. 靜態(tài)內(nèi)部類

利用 Java 的靜態(tài)內(nèi)部類赋续。它有點類似餓漢式,但又能做到了延遲加載另患。具體是怎么做到的呢纽乱?我們先來看它的代碼實現(xiàn)。

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

  private static class SingletonHolder{
    private static final IdGenerator instance = new IdGenerator();
  }
  
  public static IdGenerator getInstance() {
    return SingletonHolder.instance;
  }
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

SingletonHolder 是一個靜態(tài)內(nèi)部類昆箕,當外部類 IdGenerator 被加載的時候鸦列,并不會創(chuàng)建 SingletonHolder 實例對象。只有當調(diào)用 getInstance() 方法時鹏倘,SingletonHolder 才會被加載薯嗤,這個時候才會創(chuàng)建 instance。instance 的唯一性纤泵、創(chuàng)建過程的線程安全性骆姐,都由 JVM 來保證镜粤。所以,這種實現(xiàn)方法既保證了線程安全玻褪,又能做到延遲加載肉渴。

5. 枚舉

這種實現(xiàn)方式通過 Java 枚舉類型本身的特性,保證了實例創(chuàng)建的線程安全性和實例的唯一性带射。具體的代碼如下所示:

public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

單例存在哪些問題?

在項目中使用單例同规,都是用它來表示一些全局唯一類,比如配置信息類窟社、連接池類券勺、ID 生成器類。單例模式書寫簡潔灿里、使用方便关炼,在代碼中,我們不需要創(chuàng)建對象匣吊,直接通過類似 IdGenerator.getInstance().getId() 這樣的方法來調(diào)用就可以了儒拂。但是,這種使用方法有點類似硬編碼(hard code)缀去,會帶來諸多問題侣灶。接下來,我們就具體看看到底有哪些問題:

1. 單例對 OOP 特性的支持不友好

OOP 的四大特性是封裝缕碎、抽象褥影、繼承、多態(tài)咏雌。單例這種設(shè)計模式對于其中的抽象凡怎、繼承、多態(tài)都支持得不好赊抖。為什么這么說呢统倒?我們還是通過 IdGenerator 這個例子來講解。

public class Order {
  public void create(...) {
    //...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

public class User {
  public void create(...) {
    // ...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

IdGenerator 的使用方式違背了基于接口而非實現(xiàn)的設(shè)計原則氛雪,也就違背了廣義上理解的 OOP 的抽象特性房匆。如果未來某一天,我們希望針對不同的業(yè)務(wù)采用不同的 ID 生成算法报亩。比如浴鸿,訂單 ID 和用戶 ID 采用不同的 ID 生成器來生成。為了應(yīng)對這個需求變化弦追,我們需要修改所有用到 IdGenerator 類的地方岳链,這樣代碼的改動就會比較大。

public class Order {
  public void create(...) {
    //...
    long id = IdGenerator.getInstance().getId();
    // 需要將上面一行代碼劲件,替換為下面一行代碼
    long id = OrderIdGenerator.getIntance().getId();
    //...
  }
}

public class User {
  public void create(...) {
    // ...
    long id = IdGenerator.getInstance().getId();
    // 需要將上面一行代碼掸哑,替換為下面一行代碼
    long id = UserIdGenerator.getIntance().getId();
  }
}

一旦你選擇將某個類設(shè)計成到單例類约急,也就意味著放棄了繼承和多態(tài)這兩個強有力的面向?qū)ο筇匦裕簿拖喈斢趽p失了可以應(yīng)對未來需求變化的擴展性苗分。

2. 單例會隱藏類之間的依賴關(guān)系

通過構(gòu)造函數(shù)厌蔽、參數(shù)傳遞等方式聲明的類之間的依賴關(guān)系,我們通過查看函數(shù)的定義俭嘁,就能很容易識別出來躺枕。但是服猪,單例類不需要顯示創(chuàng)建供填、不需要依賴參數(shù)傳遞,在函數(shù)中直接調(diào)用就可以了罢猪。如果代碼比較復(fù)雜近她,這種調(diào)用關(guān)系就會非常隱蔽。在閱讀代碼的時候膳帕,我們就需要仔細查看每個函數(shù)的代碼實現(xiàn)粘捎,才能知道這個類到底依賴了哪些單例類。

3. 單例對代碼的擴展性不友好

我們知道危彩,單例類只能有一個對象實例攒磨。如果未來某一天,我們需要在代碼中創(chuàng)建兩個實例或多個實例汤徽,那就要對代碼有比較大的改動娩缰。你可能會說,會有這樣的需求嗎谒府?既然單例類大部分情況下都用來表示全局類拼坎,怎么會需要兩個或者多個實例呢?

實際上完疫,這樣的需求并不少見泰鸡。我們拿數(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. 單例對代碼的可測試性不友好

單例模式的使用會影響到代碼的可測試性邑狸。如果單例類依賴比較重的外部資源,比如 DB涤妒,我們在寫單元測試的時候单雾,希望能通過 mock 的方式將它替換掉。而單例類這種硬編碼式的使用方式届腐,導(dǎo)致無法實現(xiàn) mock 替換铁坎。

除此之外,如果單例類持有成員變量(比如 IdGenerator 中的 id 成員變量)犁苏,那它實際上相當于一種全局變量硬萍,被所有的代碼共享。如果這個全局變量是一個可變?nèi)肿兞课辏簿褪钦f朴乖,它的成員變量是可以被修改的,那我們在編寫單元測試的時候助赞,還需要注意不同測試用例之間买羞,修改了單例類中的同一個成員變量的值,從而導(dǎo)致測試結(jié)果互相影響的問題雹食。

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

單例不支持有參數(shù)的構(gòu)造函數(shù)畜普,比如我們創(chuàng)建一個連接池的單例對象,我們沒法通過參數(shù)來指定連接池的大小群叶。

有何替代解決方案吃挑?
// 1. 老的使用方式
public demofunction() {
  //...
  long id = IdGenerator.getInstance().getId();
  //...
}

// 2. 新的使用方式:依賴注入
public demofunction(IdGenerator idGenerator) {
  long id = idGenerator.getId();
}
// 外部調(diào)用demofunction()的時候钝荡,傳入idGenerator
IdGenerator idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);

基于新的使用方式,我們將單例生成的對象舶衬,作為參數(shù)傳遞給函數(shù)(也可以通過構(gòu)造函數(shù)傳遞給類的成員變量)埠通,可以解決單例隱藏類之間依賴關(guān)系的問題。不過逛犹,對于單例存在的其他問題端辱,比如對 OOP 特性、擴展性虽画、可測性不友好等問題舞蔽,還是無法解決。

所以狸捕,如果要完全解決這些問題喷鸽,我們可能要從根上众雷,尋找其他方式來實現(xiàn)全局唯一類灸拍。實際上,類對象的全局唯一性可以通過多種不同的方式來保證砾省。我們既可以通過單例模式來強制保證鸡岗,也可以通過工廠模式、IOC 容器(比如 Spring IOC 容器)來保證编兄。

相關(guān)推薦

本文轉(zhuǎn)自 https://juejin.cn/post/7040651819193729061轩性,如有侵權(quán),請聯(lián)系刪除狠鸳。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子沥寥,更是在濱河造成了極大的恐慌她倘,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铅祸,死亡現(xiàn)場離奇詭異坑质,居然都是意外死亡,警方通過查閱死者的電腦和手機临梗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門涡扼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盟庞,你說我怎么就攤上這事吃沪。” “怎么了什猖?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵票彪,是天一觀的道長萎津。 經(jīng)常有香客問我,道長抹镊,這世上最難降的妖魔是什么锉屈? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮垮耳,結(jié)果婚禮上颈渊,老公的妹妹穿的比我還像新娘。我一直安慰自己终佛,他們只是感情好俊嗽,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著铃彰,像睡著了一般绍豁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上牙捉,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天竹揍,我揣著相機與錄音,去河邊找鬼邪铲。 笑死芬位,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的带到。 我是一名探鬼主播昧碉,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼揽惹!你這毒婦竟也來了被饿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤搪搏,失蹤者是張志新(化名)和其女友劉穎狭握,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體慕嚷,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡哥牍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了喝检。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嗅辣。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖挠说,靈堂內(nèi)的尸體忽然破棺而出澡谭,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布蛙奖,位于F島的核電站潘酗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏雁仲。R本人自食惡果不足惜仔夺,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望攒砖。 院中可真熱鬧缸兔,春花似錦、人聲如沸吹艇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽受神。三九已至抛猖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鼻听,已是汗流浹背财著。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留精算,地道東北人瓢宦。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像灰羽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鱼辙,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容