單例的幾種寫法

Gevin發表於2021-10-13

「核心設計」

把建構函式設定為私有,不允許外部建立類的例項,透過靜態方法getInstance()獲取唯一例項。


enter image description here

1. 餓漢式

設計類的私有靜態例項instance,在類載入的時候,instance 就已經建立並初始化好了,所以,instance例項的建立過程是執行緒安全的。

透過靜態方法getInstance()返回instance時,不存在競態條件,也是執行緒安全的。

優點:簡單,執行緒安全,效能高

缺點:不支援延遲載入,如果例項佔用資源多或初始化耗時長,提前初始化例項是資源


public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

2. 懶漢式

不事先初始化instance,在getInstance()方法中增加一步,判斷如果instance==null,則先建立例項。

優點:簡單,支援懶載入

缺點:為保證執行緒安全,getInstance()需要上鎖同步,併發度低,當單例被頻繁用到時,會成為效能瓶頸


public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  public static synchronized IdGenerator getInstance() {
    if (instance == null) {
      instance = new IdGenerator();
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

3. 雙重檢測

為提高懶漢式的併發能力,減小鎖的顆粒度。

優點:既支援懶載入,又支援高併發

缺點:程式碼邏輯複雜,容易寫錯


public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    if (instance == null) {
      synchronized(IdGenerator.class) { // 此處為類級別的鎖
        if (instance == null) {
          instance = new IdGenerator();
        }
      }
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

說明:

  1. 同步塊內外兩次檢查instance==null,是為了防止出現併發時,幾個執行緒相繼獲得鎖進入同步塊後,分別建立instance例項,多次賦值
  2. 本程式碼在多執行緒條件下存在競態條件,需要考慮instance變數的可見性和賦值的原子性,故程式碼邏輯複雜,易出錯:
    1. instance要對全部執行緒可見,這個透過synchronizedvolatile關鍵字保證,本程式碼基於前者
    2. 賦值的原子性,即instance = new IdGenerator()這一行,在舊版本的java(java7及以下),本語句編譯後不是原子的,若發生指令重排,其他執行緒可能拿到錯誤的instance物件
  3. 基於第2-2點,舊版本的java中,為保證併發時的正確性,instance的宣告語句需要volatile關鍵字避免指令重排,即要改為private volatile static IdGenerator instance;

4. 使用靜態內部類

使用靜態內部類,能達到“雙重檢測”一樣的效果,且實現更簡單,不易出錯。

優點:高併發、執行緒安全、支援懶載入、簡單

缺點:暫無


public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private IdGenerator() {}

  private static class SingletonHolder{
    private static final IdGenerator instance = new IdGenerator();
  }

  public static IdGenerator getInstance() {
    return SingletonHolder.instance;
  }

  public long getId() { 
    return id.incrementAndGet();
  }
}

說明:

  1. SingletonHolder 是一個靜態內部類,當外部類 IdGenerator被載入的時候,並不會建立 SingletonHolder 例項物件;只有當呼叫 getInstance() 方法時,SingletonHolder 才會被載入,這個時候才會建立 instance
  2. instance 的唯一性、建立過程的執行緒安全性,都由 JVM 來保證
  3. 現在若使用Intelij Idea 寫單例,寫一個“雙重檢測”的單例實現後,idea會自動提示重構為“使用內部靜態類”的方式

5. 其他方法

對於Java語言而言,還可以透過列舉型別實現單例,它是透過 Java 列舉型別本身的特性,保證了例項建立的執行緒安全性和例項的唯一性:


public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);

  public long getId() { 
    return id.incrementAndGet();
  }
}

What’s More

本文程式碼,直接使用了極客時間王爭老師的專欄《設計模式之美》,點選的『這裡』可查閱,也可以掃下面二維碼訂閱。

極客時間-設計模式之美


注:轉載本文,請與Gevin聯絡




如果您覺得Gevin的文章有價值,就請Gevin喝杯茶吧!

|

歡迎關注我的微信公眾賬號

單例的幾種寫法

相關文章