需要搞清楚的疑問
- 為什么要使用單例?
- 單例存在哪些問題?
- 單例與靜態(tài)類的區(qū)別?
- 有何替代的解決方案?
- 如何理解單例模式中的唯一性
- 如何實現(xiàn)線程唯一的單例
- 如何實現(xiàn)集群環(huán)境下的單例
- 如何實現(xiàn)一個多例模式
為什么要使用單例?
定義
一個類只允許創(chuàng)建一個對象,這個類就是就是一個單例類,這種設(shè)計模式就是單例設(shè)計模式-
用處
- 從業(yè)務(wù)概念上,有些數(shù)據(jù)在系統(tǒng)中只應(yīng)該保存一份,就比較適合設(shè)計為單例類。比如,系統(tǒng)的配置信息類
- 使用單例解決資源訪問沖突的問題
實現(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;
}
}
單例存在哪些問題?
單例對 OOP 特性的支持不友好
一旦你選擇將某個類設(shè)計成到單例類,也就意味著放棄了繼承和多態(tài)這兩個強有力的面向?qū)ο筇匦?,也就相?dāng)于損失了可以應(yīng)對未來需求變化的擴展性單例會隱藏類之間的依賴關(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),才能知道這個類到底依賴了哪些單例類。單例對代碼的擴展性不友好
數(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è)計成單例類。單例對代碼的可測試性不友好
單例不支持有參數(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)建的是不同子類的對象