單例設(shè)計模式理解起來非常簡單。一個類只允許創(chuàng)建一個對象(或者實例),那這個類就是一個單例類,這種設(shè)計模式就叫單例模式。
使用場景
處理資源訪問沖突
下面的示例中如果每個類都創(chuàng)建一個 Logger 實例,就可能造成日志內(nèi)容被覆蓋的情況。
public?class?Logger?{??private?FileWriter?writer;??public?Logger()?{????File?file?=?new?File("log.txt");????writer?=?new?FileWriter(file,?true);?//true表示追加寫入??}??public?void?log(String?message)?{????writer.write(mesasge);??}}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());??}}復(fù)制代碼
表示全局唯一類
如果有些數(shù)據(jù)在系統(tǒng)中只應(yīng)保存一份,那就比較適合設(shè)計為單例類。比如,配置信息類,全局 ID 生成器等。
如何實現(xiàn)一個單例?
要實現(xiàn)一個單例,我們要考慮以下幾點:
構(gòu)造函數(shù)需要是 private 訪問權(quán)限的,這樣才能避免外部通過 new 創(chuàng)建實例;
考慮對象創(chuàng)建時的線程安全問題;
考慮是否支持延遲加載;
考慮 getInstance() 性能是否高(是否加鎖)。
餓漢式
public?class?Singleton?{??private?static?final?Singleton?instance?=?new?Singleton();??private?Singleton()?{}??public?static?Singleton?getInstance()?{????return?instance;??}}復(fù)制代碼
懶漢式
懶漢式相對于餓漢式的優(yōu)勢是**「支持延遲加載」。但缺點也很明顯,因為使用了synchronized關(guān)鍵字導(dǎo)致這個方法的「并發(fā)度很低」**。如果這個單例類偶爾會被用到,那這種實現(xiàn)方式還可以接受。但是,如果頻繁地用到,就會導(dǎo)致性能瓶頸,這種實現(xiàn)方式就不可取了。
public?class?Singleton?{??private?static?Singleton?instance;??private?Singleton()?{}??public?static?synchronized?Singleton?getInstance()?{????if?(instance?==?null)?{??????instance?=?new?Singleton();????}????return?instance;??}}復(fù)制代碼
雙重檢測
這是一種既支持延遲加載、又支持高并發(fā)的單例實現(xiàn)方式。
public?class?Singleton?{??private?static?Singleton?instance;??private?Singleton()?{}??public?static?Singleton?getInstance()?{????if?(instance?==?null)?{??????synchronized(Singleton.class)?{?//?此處為類級別的鎖????????if?(instance?==?null)?{??????????instance?=?new?Singleton();????????}??????}????}????return?instance;??}}復(fù)制代碼
在 java1.5 以下instance = new Singleton();有指令重排問題,需要給instance成員變量加上volatile關(guān)鍵字,java1.5 之后不會再這個有問題。
靜態(tài)內(nèi)部類
這種方式利用了 Java 的靜態(tài)內(nèi)部類,有點類似餓漢式,但又能做到了延遲加載。
當(dāng)外部類 Singleton 被加載的時候,并不會創(chuàng)建 SingletonHolder 實例對象。只有當(dāng)調(diào)用 getInstance() 方法時,SingletonHolder 才會被加載,這個時候才會創(chuàng)建 instance。insance 的唯一性、創(chuàng)建過程的線程安全性,都由 JVM 來保證。所以,這種實現(xiàn)方法既保證了線程安全,又能做到延遲加載。
public?class?Singleton?{??private?Singleton()?{}??private?static?class?SingletonHolder{????private?static?final?Singleton?instance?=?new?Singleton();??}??public?static?Singleton?getInstance()?{????return?SingletonHolder.instance;??}}復(fù)制代碼
枚舉
這是一種最簡單的實現(xiàn)方式,基于枚舉類型的單例實現(xiàn)。這種實現(xiàn)方式通過 Java 枚舉類型本身的特性,保證了實例創(chuàng)建的線程安全性和實例的唯一性。
public?enum?IdGenerator?{??INSTANCE;??private?AtomicLong?id?=?new?AtomicLong(0);??public?long?getId()?{????return?id.incrementAndGet();??}}復(fù)制代碼
如何實現(xiàn)線程唯一的單例?
上面的單例類對象是進(jìn)程唯一的,一個進(jìn)程只能有一個單例對象。那如何實現(xiàn)一個線程唯一的單例呢?
假設(shè) IdGenerator 是一個線程唯一的單例類。在線程 A 內(nèi),我們可以創(chuàng)建一個單例對象 a。因為線程內(nèi)唯一,在線程 A 內(nèi)就不能再創(chuàng)建新的 IdGenerator 對象了,而線程間可以不唯一,所以,在另外一個線程 B 內(nèi),我們還可以重新創(chuàng)建一個新的單例對象 b。
我們通過一個 ConcurrentHashMap 來存儲對象,其中 key 是線程 ID,value 是對象。這樣我們就可以做到,不同的線程對應(yīng)不同的對象,同一個線程只能對應(yīng)一個對象。實際上,Java 語言本身提供了 ThreadLocal 工具類,可以更加輕松地實現(xiàn)線程唯一單例。
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();
??}
}
作者:開發(fā)者充電站
鏈接:https://juejin.cn/post/7053363491788292127