單例模式的幾種實現方式(JAVA)

單例模式的幾種實現方式(JAVA)

概括起來,要實現一個單例,我們需要關注的點無外乎下面幾個:

  • 構造函數需要是 private 訪問權限的,這樣才能避免外部通過 new 創建實例;
  • 考慮對象創建時的線程安全問題;考慮是否支持延遲加載;
  • 考慮 getInstance() 性能是否高(是否加鎖)。

1、餓漢式

餓漢式的實現方式比較簡單。在類加載的時候,instance 靜態實例就已經創建並初始化好了,所以,instance 實例的創建過程是線程安全的。不過,這樣的實現方式不支持延遲加載(在真正用到 IdGenerator 的時候,再創建實例),從名字中我們也可以看出這一點。具體的代碼實現如下所示:


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

有人覺得這種實現方式不好,因爲不支持延遲加載,如果實例佔用資源多(比如佔用內存多)或初始化耗時長(比如需要加載各種配置文件),提前初始化實例是一種浪費資源的行爲。最好的方法應該在用到的時候再去初始化。不過,我個人並不認同這樣的觀點。

如果初始化耗時長,那我們最好不要等到真正要用它的時候,纔去執行這個耗時長的初始化過程,這會影響到系統的性能(比如,在響應客戶端接口請求的時候,做這個初始化操作,會導致此請求的響應時間變長,甚至超時)。採用餓漢式實現方式,將耗時的初始化操作,提前到程序啓動的時候完成,這樣就能避免在程序運行的時候,再去初始化導致的性能問題。

如果實例佔用資源多,按照 fail-fast 的設計原則(有問題及早暴露),那我們也希望在程序啓動時就將這個實例初始化好。如果資源不夠,就會在程序啓動的時候觸發報錯(比如 Java 中的 PermGen Space OOM),我們可以立即去修復。這樣也能避免在程序運行一段時間後,突然因爲初始化這個實例佔用資源過多,導致系統崩潰,影響系統的可用性。

2、懶漢式

有餓漢式,對應的,就有懶漢式。懶漢式相對於餓漢式的優勢是支持延遲加載。具體的代碼實現如下所示:


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

不過懶漢式的缺點也很明顯,我們給 getInstance() 這個方法加了一把大鎖(synchronzed),導致這個函數的併發度很低。量化一下的話,併發度是 1,也就相當於串行操作了。而這個函數是在單例使用期間,一直會被調用。如果這個單例類偶爾會被用到,那這種實現方式還可以接受。但是,如果頻繁地用到,那頻繁加鎖、釋放鎖及併發度低等問題,會導致性能瓶頸,這種實現方式就不可取了。

3、雙重檢測

餓漢式不支持延遲加載,懶漢式有性能問題,不支持高併發。那我們再來看一種既支持延遲加載、又支持高併發的單例實現方式,也就是雙重檢測實現方式。在這種實現方式中,只要 instance 被創建之後,即便再調用 getInstance() 函數也不會再進入到加鎖邏輯中了。所以,這種實現方式解決了懶漢式併發度低的問題。具體的代碼實現如下所示:


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

4、靜態內部類

我們再來看一種比雙重檢測更加簡單的實現方法,那就是利用 Java 的靜態內部類。它有點類似餓漢式,但又能做到了延遲加載。具體是怎麼做到的呢?我們先來看它的代碼實現。


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

SingletonHolder 是一個靜態內部類,當外部類 IdGenerator 被加載的時候,並不會創建 SingletonHolder 實例對象。只有當調用 getInstance() 方法時,SingletonHolder 纔會被加載,這個時候纔會創建 instance。instance 的唯一性、創建過程的線程安全性,都由 JVM 來保證。所以,這種實現方法既保證了線程安全,又能做到延遲加載。

5、枚舉

最後,我們介紹一種最簡單的實現方式,基於枚舉類型的單例實現。這種實現方式通過 Java 枚舉類型本身的特性,保證了實例創建的線程安全性和實例的唯一性。具體的代碼如下所示:


public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

參考自《設計模式之美》王爭

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