單例模式你真的會了嗎?(下篇)

距離《單例模式上篇》寫出去已經很久了,竟然久久沒有更新下篇,這是庸俗人的普遍表現,只有開始,沒有繼續,也沒有結束;幹什麼事都沒有恆心,只有三天熱度。要堅持啊!

《單例模式上篇》描述了單例的幾個核心問題:

爲什麼要有單例?

正確單例應該怎麼寫?

典型的單例模式寫法?

接下來,我們來進階一下,拓展一下單例的高級用法,所謂開拓思路,不亦樂乎嘛!

  • 單例模式的唯一性如何理解?
  • 線程唯一的單例怎麼實現?
  • 如何實現集羣模式下的單例?
  • 怎麼實現“多例”模式?

看着是不是有些頭大,不要急,聽我慢慢道來。

單例模式的唯一性怎麼理解

我們先來看下單例的定義:“一個類只允許創建唯一一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。”

一個類只允許創建唯一一個對象,這個唯一性的範圍是什麼?我們都知道程序運行的最小單位是線程,平時編寫的一個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的特性支持並不友好,隱藏類之間的依賴關係,擴展性差,可測試性也不好,也不支持有參數的構造函數(一般的單例構造函數都是私有的)。

每一種設計模式,並不是是用的越多越好,不要爲了使用設計模式而過渡濫用設計模式,不要爲了設計而設計,要明白每一種設計模式是爲了解決什麼問題,爲什麼用,如何用,怎麼用對用好。

通過舉一反三,也可以應用到在學習和工作中,不是爲了工作而工作,要抱着解決問題,提升自己的態度去工作,每一件事都不好做,捨我其誰!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章