設計模式學習筆記(三):單例模式

1 概述

1.1 引言

不少時候爲了節約系統資源,須要確保系統中某個類只有一個惟一的實例,當這個惟一實例建立了以後,沒法再建立一個同類型的其餘對象,全部的操做只能基於這一個惟一實例。這是單例模式的動機所在。java

好比Windows的任務管理器,能夠按Ctrl+Shift+Esc啓動,並且啓動一個,不能啓動多個。編程

1.2 定義

單例模式(Singleton Pattern):確保某一個類只有一個實例,並且自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。安全

單例模式是一種對象建立型模式。微信

1.3 結構圖

在這裏插入圖片描述

1.4 角色

單例模式只有一個角色:多線程

  • Singleton(單例角色):在單例類的內部只生成一個實例,同時它提供一個相似名叫getInstance的靜態方法獲取實例,同時爲了防止外部生成新的實例化對象,構造方法可見性爲private,在單例類內部定義了一個Singleton的靜態對象,做爲供外部訪問的惟一實例

2 典型實現

2.1 步驟

  • 構造函數私有化:也就是禁止外部直接使用new等方式建立對象
  • 定義靜態成員:定義一個私有靜態成員保存實例
  • 增長公有靜態方法:增長一個相似getInstance()的公有靜態方法來獲取實例

2.2 單例角色

單例角色一般實現以下:併發

class Singleton
{
    //餓漢式實現
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance()
    {
        return instance;
    }
}

2.3 客戶端

客戶端直接經過該類獲取實例便可:負載均衡

Singleton singleton = Singleton.getInstance();

3 實例

某個軟件須要使用一個全局惟一的負載均衡器,使用單例模式對其進行設計。

代碼以下:編程語言

public class LoadBalancer
{
    private static LoadBalancer instance = null;

    private LoadBalancer(){}

    public static LoadBalancer getInstance()
    {
        return instance == null ? instance = new LoadBalancer() : instance;
    }

    public static void main(String[] args) {
        LoadBalancer balancer1 = LoadBalancer.getInstance();
        LoadBalancer balancer2 = LoadBalancer.getInstance();
        System.out.println(balancer1 == balancer2);
    }
}

這是最簡單的單例類的設計,獲取實例時僅僅判斷是否爲null,沒有考慮到線程問題。也就是說,多個線程同時獲取實例時,仍是有可能會產生多個實例,通常來講,常見的解決方式以下:函數

  • 餓漢式單例
  • 懶漢式單例
  • IoDH

4 餓漢式單例

餓漢式單例就是在普通的單例類基礎上,在定義靜態變量時就直接實例化,所以在類加載的時候就已經建立了單例對象,並且在獲取實例時不須要進行判空操做直接返回實例便可:性能

public class LoadBalancer
{
    private static LoadBalancer instance = new LoadBalancer();

    private LoadBalancer(){}

    public static LoadBalancer getInstance()
    {
        return instance;
    }
}

當類被加載時,靜態變量instance被初始化,類的私有構造方法將被調用,單例類的惟一實例將被建立。

5 懶漢式單例

懶漢式單例在類加載時不進行初始化,在須要的時候再初始化,加載實例,同時爲了不多個線程同時調用getInstance(),能夠加上synchronized

public class LoadBalancer
{
    private static LoadBalancer instance = null;

    private LoadBalancer(){}

    synchronized public static LoadBalancer getInstance()
    {
        return instance == null ? instance = new LoadBalancer() : instance;
    }
}

這種技術又叫延遲加載技術,儘管解決了多個線程同時訪問的問題,可是每次調用時都須要進行線程鎖定判斷,這樣會下降效率。

事實上,單例的核心在於instance = new LoadBalancer(),所以只須要鎖定這行代碼,優化以下:

public static LoadBalancer getInstance()
{
    if(instance == null)
    {
        synchronized (LoadBalancer.class)
        {
            instance = new LoadBalancer();
        }
    }
    return instance;
}

可是實際狀況中仍是有可能出現多個實例,由於若是A和B兩個線程同時調用getInstance(),都經過了if(instance == null)的判斷,假設線程A先得到鎖,建立實例後,A釋放鎖,接着B獲取鎖,再次建立了一個實例,這樣仍是致使產生多個單例對象。

所以,一般採用一種叫「雙重檢查鎖定」的方式來確保不會產生多個實例,一個線程獲取鎖後再進行一次判空操做:

private volatile static LoadBalancer instance = null;
public static LoadBalancer getInstance()
{
    if(instance == null)
    {
        synchronized (LoadBalancer.class)
        {
            if(instance == null)
            {
                instance = new LoadBalancer();
            }
        }
    }
    return instance;
}

須要注意的是要使用volatile修飾變量,volatile能夠保證可見性以及有序性。

6 餓漢式與懶漢式的比較

  • 餓漢式在類加載時就已經初始化,優勢在於無需考慮多線程訪問問題,能夠確保實例的惟一性
  • 從調用速度方面來講餓漢式會優於懶漢式,由於在類加載時就已經被建立
  • 從資源利用效率來講餓漢式會劣於懶漢式,由於不管是否須要使用都會加載單例對象,並且因爲加載時須要建立實例會致使類加載時間變長
  • 懶漢式實現了延遲加載,無須一直佔用系統資源
  • 懶漢式須要處理多線程併發訪問問題,須要雙重檢查鎖定,且一般來講初始化過程須要較長時間,會增大多個線程同時首次調用的概率,這會致使系統性能受必定影響

7 IoDH

爲了克服餓漢式不能延遲加載以及懶漢式的線程安全控制繁瑣問題,可使用一種叫Initialization on Demand Holder(IoDH)的技術。實現IoDH時,需在單例類增長一個靜態內部類,在該內部類中建立單例對象,再將該單例對象經過getInstance()方法返回給外部使用,代碼以下:

public class LoadBalancer
{
    private LoadBalancer(){}
    private static class HolderClass
    {
        private static final LoadBalancer instance = new LoadBalancer();
    }
    
    public static LoadBalancer getInstance()
    {
        return HolderClass.instance;
    }
}

因爲單例對象沒有做爲LoadBalancer的成員變量直接實例化,所以類加載時不會實例化instance。首次調用getInstance()時,會初始化instance,由JVM保證線程安全性,確保只能被初始化一次。另外相比起懶漢式單例,getInstance()沒有線程鎖定,所以性能不會有任何影響。

經過IoDH既能夠實現延遲加載,又能夠保證線程安全,不影響系統性能,可是缺點是與編程語言自己的特性相關,不少面嚮對象語言不支持IoDH。另外,還可能引起NoClassDefFoundError(當初始化失敗時),例子能夠戳這裏

8 枚舉實現單例(推薦)

其中,不管是餓漢式,仍是懶漢式,仍是IoDH,都有或多或少的問題,而且還能夠經過反射以及序列化/反序列化方式去「強制」生成多個單例,有沒有更優雅的解決方案呢?

有!答案就是枚舉。

代碼以下:

public class Test
{
    public static void main(String[] args) {
        LoadBalancer balancer1 = LoadBalancer.INSTANCE;
        LoadBalancer balancer2 = LoadBalancer.INSTANCE;
        System.out.println(balancer1 == balancer2);
    }
}

enum LoadBalancer{
    INSTANCE;
}

使用枚舉實現單例優勢以下:

  • 代碼簡潔不易出錯
  • 無須像餓漢式同樣直接在類加載時初始化
  • 也無須像懶漢式同樣須要雙重檢查鎖定
  • 也無須像IoDH同樣添加一個靜態內部類增長系統中類的數量
  • 由JVM保證線程安全
  • 不會由於序列化生成新實例
  • 也不會由於反射生產新實例

9 主要優勢

  • 惟一實例:單例模式提供了對惟一實例的受控訪問,能夠嚴格控制客戶怎樣以及什麼時候訪問它
  • 節約資源:因爲在系統內存中只存在一個對象,所以能夠節約系統資源,對於一些須要頻繁建立和銷燬的對象,單例模式能夠提升系統性能

10 主要缺點

  • 擴展困難:沒有抽象層,擴展困難
  • 職責太重:單例類職責太重,必定程度上違反了SRP,由於既提供了業務方法,也提供了建立對象方法,將對象建立以及對象自己的功能耦合在一塊兒
  • GC致使從新實例化:不少語言提供了GC機制,實例化的對象長時間不使用將被回收,下次使用須要從新實例化,這回致使共享的單例對象狀態丟失

11 適用場景

  • 系統須要一個實例對象
  • 客戶調用類的單個實例只容許使用一個公共訪問點

12 總結

在這裏插入圖片描述

若是以爲文章好看,歡迎點贊。

同時歡迎關注微信公衆號:氷泠之路。

在這裏插入圖片描述

相關文章
相關標籤/搜索