單例模式的五種實現方式及優缺點

公號:碼農充電站pro
主頁:https://codeshellme.github.iohtml

當咱們須要使得某個類只能有一個實例時,可使用單例模式java

單例模式Singleton Design Pattern)保證一個類只能有一個實例,並提供一個全局訪問點。git

單例模式的實現須要三個必要的條件github

  1. 單例類的構造函數必須是私有的,這樣才能將類的建立權控制在類的內部,從而使得類的外部不能建立類的實例。
  2. 單例類經過一個私有的靜態變量來存儲其惟一實例。
  3. 單例類經過提供一個公開的靜態方法,使得外部使用者能夠訪問類的惟一實例。

注意:
由於單例類的構造函數是私有的,因此單例類不能被繼承。shell

另外,實現單例類時,還須要考慮三個問題:數據庫

  • 建立單例對象時,是否線程安全。
  • 單例對象的建立,是否延時加載。
  • 獲取單例對象時,是否須要加鎖(鎖會致使低性能)。

下面介紹五種實現單例模式的方式。設計模式

1,餓漢式

餓漢式的單例實現比較簡單,其在類加載的時候,靜態實例instance 就已建立並初始化好了。安全

代碼以下:多線程

public class Singleton { 
  private static final Singleton instance = new Singleton();
  
  private Singleton () {}
  
  public static Singleton getInstance() {
    return instance;
  }
}

餓漢式單例優缺點:併發

  • 優勢:
    • 單例對象的建立是線程安全的;
    • 獲取單例對象時不須要加鎖。
  • 缺點:單例對象的建立,不是延時加載。

通常認爲延時加載能夠節省內存資源。可是延時加載是否是真正的好,要看實際的應用場景,而不必定全部的應用場景都須要延時加載。

2,懶漢式

與餓漢式對應的是懶漢式,懶漢式爲了支持延時加載,將對象的建立延遲到了獲取對象的時候,但爲了線程安全,不得不爲獲取對象的操做加鎖,這就致使了低性能。

而且這把鎖只有在第一次建立對象時有用,而以後每次獲取對象,這把鎖都是一個累贅(雙重檢測對此進行了改進)。

代碼以下:

public class Singleton { 
  private static final Singleton instance;
  
  private Singleton () {}
  
  public static synchronized Singleton getInstance() {    
    if (instance == null) {      
      instance = new Singleton();    
    }    

    return instance;  
  }
}

懶漢式單例優缺點:

  • 優勢:
    • 對象的建立是線程安全的。
    • 支持延時加載。
  • 缺點:獲取對象的操做被加上了鎖,影響了併發度。
    • 若是單例對象須要頻繁使用,那這個缺點就是沒法接受的。
    • 若是單例對象不須要頻繁使用,那這個缺點也無傷大雅。

3,雙重檢測

餓漢式和懶漢式的單例都有缺點,雙重檢測的實現方式解決了這二者的缺點。

雙重檢測將懶漢式中的 synchronized 方法改爲了 synchronized 代碼塊。以下:

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;
  }
}

這種實現方式在 Java 1.4 及更早的版本中有些問題,就是指令重排序,可能會致使 Singleton 對象被 new 出來,而且賦值給 instance 以後,還沒來得及初始化,就被另外一個線程使用了。

要解決這個問題,須要給 instance 成員變量加上 volatile 關鍵字,從而禁止指令重排序。

而高版本的 Java 已在 JDK 內部解決了這個問題,因此高版本的 Java 不須要關注這個問題。

雙重檢測單例優勢:

  • 對象的建立是線程安全的。
  • 支持延時加載。
  • 獲取對象時不須要加鎖。

4,靜態內部類

用靜態內部類的方式實現單例類,利用了Java 靜態內部類的特性:

  • Java 加載外部類的時候,不會建立內部類的實例,只有在外部類使用到內部類的時候纔會建立內部類實例

代碼以下:

public class Singleton { 
  private Singleton () {}

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

SingletonInner 是一個靜態內部類,當外部類 Singleton 被加載的時候,並不會建立 SingletonInner 實例對象。

只有當調用 getInstance() 方法時,SingletonInner 纔會被加載,這個時候纔會建立 instanceinstance 的惟一性、建立過程的線程安全性,都由 JVM 來保證。

靜態內部類單例優勢:

  • 對象的建立是線程安全的。
  • 支持延時加載。
  • 獲取對象時不須要加鎖。

5,枚舉

用枚舉來實現單例,是最簡單的方式。這種實現方式經過 Java 枚舉類型自己的特性,保證了實例建立的線程安全性和實例的惟一性。

public enum Singleton {
  INSTANCE; // 該對象全局惟一
}

6,多例模式

上面介紹了5 種單例模式的實現方式,下面做爲對單例模式的擴展,再來介紹一下多例模式以及線程間惟一的單例模式。先來看下多例模式。

單例模式是指,一個類只能建立一個對象。那麼多例模式就是,一個類能夠建立多個對象,可是對象個數能夠控制。

對於多例模式,咱們能夠將類的實例都編上號,而後將實例存放在一個 Map 中。

代碼以下:

public class MultiInstance {
    // 實例編號
    private long instanceNum;

    // 用於存放實例
    private static final Map<Long, MultiInstance> ins = new HashMap<>();

    static {
        // 存放 3 個實例
        ins.put(1L, new MultiInstance(1));
        ins.put(2L, new MultiInstance(2));
        ins.put(3L, new MultiInstance(3));
    }

    private MultiInstance(long n) {
        this.instanceNum = n;
    }

    public MultiInstance getInstance(long n) {
        return ins.get(n);
    }
}

實際上,Java 中的枚舉就是一個「自然」的多例模式,其中的每一項表明一個實例,以下:

public enum MultiInstance {
    ONE,
    TWO,
    THREE;
}

7,線程惟一的單例

通常狀況下,咱們所說的單例的做用範圍是進程惟一的,就是在一個進程範圍內,一個類只容許建立一個對象,進程內的多個線程之間也是共享同一個實例。

實際上,在Java 中,每一個類加載器都定義了一個命名空間。因此咱們這裏實現的單例是依賴類加載器的,也就是在同一個類加載器中,咱們實現的單例就是真正的單例模式。不然若是有多個類加載器,就會有多個單例出現了。一個解決辦法是:自行指定類加載器,而且指定同一個類加載器。

那麼線程惟一的單例就是,一個實例只能被一個線程擁有,一個進程內的多個線程擁有不一樣的類實例。

咱們一樣能夠用 Map 來實現,代碼以下:

public class ThreadSingleton {
    private static final ConcurrentHashMap<Long, ThreadSingleton> instances
            = new ConcurrentHashMap<>();

    private ThreadSingleton() {}

    public static ThreadSingleton getInstance() {
        Long id = Thread.currentThread().getId();
        instances.putIfAbsent(id, new ThreadSingleton());
        return instances.get(id);
    }
}

8,使用場景

單例模式能夠用來管理一些共享資源,好比數據庫鏈接池,線程池;解決資源衝突問題,好比日誌打印。節省內存空間,好比配置信息類。

(本節完。)


推薦閱讀:

設計模式之高質量代碼


歡迎關注做者公衆號,獲取更多技術乾貨。

碼農充電站pro

相關文章
相關標籤/搜索