距離《單例模式上篇》寫出去已經很久了,竟然久久沒有更新下篇,這是庸俗人的普遍表現,只有開始,沒有繼續,也沒有結束;幹什麼事都沒有恆心,只有三天熱度。要堅持啊!
《單例模式上篇》描述了單例的幾個核心問題:
爲什麼要有單例?
正確單例應該怎麼寫?
典型的單例模式寫法?
接下來,我們來進階一下,拓展一下單例的高級用法,所謂開拓思路,不亦樂乎嘛!
- 單例模式的唯一性如何理解?
- 線程唯一的單例怎麼實現?
- 如何實現集羣模式下的單例?
- 怎麼實現“多例”模式?
看着是不是有些頭大,不要急,聽我慢慢道來。
單例模式的唯一性怎麼理解
我們先來看下單例的定義:“一個類只允許創建唯一一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。”
一個類只允許創建唯一一個對象,這個唯一性的範圍是什麼?我們都知道程序運行的最小單位是線程,平時編寫的一個java程序,最終打包成一個jar包,通過jvm解釋執行,對於計算機系統來說,一個程序的運行單位就是一個進程。進程內可包含多個線程,同一個進程內的線程共享進程的內存空間;不同進程間的內存互相隔離。所以這裏說的單例模式的唯一性,指的是同一個進程內,只能創建一個單例對象。
能實現線程唯一的單例嗎?
既然平時我們編寫的程序都是進程唯一的單例,那麼問題來了?可以編寫出線程唯一的單例嗎?什麼叫線程唯一的單例呢?
比如說一個單例類SingleTon,在同一個進程內,有多個線程,假設分別是線程A、線程B、線程C…,線程A內只能創建一個SingleTon的對象,線程B內只能創建另一個SingleTon的對象…各線程間的單例對象各不相同。
你可能會問,這有什麼意義呢?還記得ThreadLocal嗎?這裏就有些類似隔離線程間的對象。廢話少說,放碼過來吧!
public class SingleTon {
private static final Map<Long, SingleTon> singleTonMap = new ConcurrentHashMap<>();
private SingleTon() {}
public static SingleTon getInstance() {
long threadId = Thread.currentThread().getId();
singleTonMap.putIfAbsent(threadId, new SingleTon());
return singleTonMap.get(threadId);
}
public void method() {}
}
集羣下唯一單例
首先什麼叫集羣下唯一的單例呢?
對比kafka、redis集羣,我們知道一個集羣可能包含多個機器,那肯定也是包含多個進程(多個線程)的了。連機器都跨越了,進程肯定都不一樣了。這實現起來好像有點難度了。因爲我們不僅要保證線程間唯一,還要保證單例對象在進程間唯一。
要保證集羣內各進程訪問單例的唯一性,首先需要保證同一時刻只有進程或線程可以獲取到單例對象,這個可以通過redis或zookeeper來實現分佈式鎖。那如何保證單例的唯一性呢?有可能是多臺機器執行同樣的程序,那單純的SingleTon單例已經無法保證唯一了,既然組成了一個集羣,那麼必然有集羣的共享存儲,如果我們將單例對象存儲到集羣的共享存儲,具體來說,進程使用單例對象時,需要將外部存儲區的實例對象讀取到內存,反序列化成對象然後使用,使用完之後,需要釋放對象,存儲回外部存儲區。
public class DistributedLock {
public void lock() {
}
public void unlock() {
}
}
public class FileSharedStorage implements SharedStorage {
private String fileName;
public FileSharedStorage(String fileName) {
this.fileName = fileName;
}
@Override
public SingleTon load(Class cls) {
return null;
}
@Override
public void save(SingleTon sharedStorage, Class cls) {
}
}
public interface SharedStorage {
SingleTon load(Class cls);
void save(SingleTon sharedStorage, Class cls);
}
public class SingleTon {
private static final String sharedFileName = "file_name";
private static SingleTon instance;
private static DistributedLock lock = new DistributedLock();
private static SharedStorage storage = new FileSharedStorage(sharedFileName);
private SingleTon() {}
public static SingleTon getInstance() {
if (instance == null) {
lock.lock();
instance = storage.load(SingleTon.class);
}
return instance;
}
public synchronized void freeInstance() {
storage.save(this, SingleTon.class);
instance = null;
lock.unlock();
}
public void method() {}
}
多例模式怎麼實現?
“單例模式”指一個類只能創建一個對象,那麼類比多例模式,就是指一個類可以創建多個對象,但是這時候創建的對象個數,一般是有限制的。
類似於,我們要實現一個隨機獲取提供服務的後臺服務器程序,每次返回的都是固定服務器對象列表中的某一個,達到將負載均衡的目的。
public class BeServer {
private int serverSequence;
private String serverAddr;
private static final int MAX_SERVER_COUNT = 5;
private static final Map<Integer, BeServer> serverMap = new HashMap<>();
static {
serverMap.put(1, new BeServer(1, "192.168.1.111:10001"));
serverMap.put(2, new BeServer(2, "192.168.1.112:10001"));
serverMap.put(3, new BeServer(3, "192.168.1.113:10001"));
}
private BeServer(int serverSequence, String serverAddr) {
this.serverSequence = serverSequence;
this.serverAddr = serverAddr;
}
public static BeServer getRandomBeServer() {
Random random = new Random();
int num = random.nextInt(MAX_SERVER_COUNT) + 1;
return serverMap.get(num);
}
}
這裏,我們可以擴展一下,平時使用的logger是怎麼實現的?針對同樣的logger name,返回的是同一個logger對象實例,如果是不同的logger name,獲取到的logger對象則是不同的。這其實也類似一種多實例模式。
public class Logger {
private static final Map<String, Logger> map = new ConcurrentHashMap<>();
private Logger() {
}
public static Logger getInstance(String loggerName) {
map.putIfAbsent(loggerName, new Logger());
return map.get(loggerName);
}
public void log() {
}
}
這種多例模式有點類似工廠模式,區別在於工廠模式創建的對象是不同子類的對象,多例模式創建的對象時同一個類的對象。
總結
單例模式看起來簡單,但是想寫出一個無bug的單例模式也不易。另外從單例模式,還可以擴展出線程單例,集羣單例,多例模式等。
另外單例模式其實並不推薦使用,因爲單例對OOP的特性支持並不友好,隱藏類之間的依賴關係,擴展性差,可測試性也不好,也不支持有參數的構造函數(一般的單例構造函數都是私有的)。
每一種設計模式,並不是是用的越多越好,不要爲了使用設計模式而過渡濫用設計模式,不要爲了設計而設計,要明白每一種設計模式是爲了解決什麼問題,爲什麼用,如何用,怎麼用對用好。
通過舉一反三,也可以應用到在學習和工作中,不是爲了工作而工作,要抱着解決問題,提升自己的態度去工作,每一件事都不好做,捨我其誰!