天天看點

單例模式 Java

  概述

  單例模式保證對于每一個類加載器,一個類僅有一個執行個體并且提供全局的通路。其是一種對象建立型模式。對于單例模式主要适用以下幾個場景:

  系統隻需要一個執行個體對象,如提供一個唯一的序列号生成器客戶調用類的單個執行個體隻允許使用一個公共通路點,除了該公共通路點,不能通過其他途徑通路該執行個體

  單例模式的缺點之一是在分布式環境中,如果因為單例模式而産生 bugs,那麼很難通過調試找出問題所在,因為在單個類加載器下進行調試,并不會出現問題。

  實作方式

  一般來說,實作枚舉有五種方式:餓漢式、懶漢式、雙重鎖檢驗、靜态内部類、枚舉,而這裡我将這五種方式分為三部分來介紹。

  餓漢式加載

  public class Singleton {

  //私有構造器,是以無法執行個體化類對象

  private Singleton() {}

  //類靜态執行個體域

  private static final Singleton INSTANCE=new Singleton();

  //傳回類執行個體

  public static Singleton getInstance() {

  return INSTANCE;

  }

  直接初始化靜态執行個體保證了線程安全,但是此種方式不是懶加載的,單例一開始就初始化了,無法在我們需要的時候再進行初始化。

  懶漢式加載

  //執行個體在這個方法第一次被調用

二手手遊買賣

的時候進行初始化

  public static synchronized Singleton getInstance() {

  if (instance==null) {

  instance=new Singleton();

  return instance;

  getInstance() 方法設定為 synchronized 保證了線程安全,但是其效率并不高,因為在任何時候隻有一個線程能夠通路這個方法,而同步操作僅需在第一次被調用的時候才被需要。

  此方法的一種改進是使用雙重鎖檢驗。

  public class ThreadSafeDoubleCheckLocking {

  private static volatile ThreadSafeDoubleCheckLocking instance;

  private ThreadSafeDoubleCheckLocking() {}

  public static ThreadSafeDoubleCheckLocking getInstance() {

  //局部變量可以提高25%的性能,這個局部變量確定instance隻在已經被初始化的情況下讀取一次

  //《Effective Java 第2版》P250頁

  ThreadSafeDoubleCheckLocking result=instance;

  //檢查執行個體是否已經别初始化

  if (result==null) {

  //未被初始化,但是無法确定這時其他線程是否已經對其初始化,是以添加對象鎖進行互斥

  synchronized (ThreadSafeDoubleCheckLocking.class) {

  //再一次将instance指派給局部變量來進行檢查,因為有可能在目前線程阻塞的時候,其他線程對instance進行初始化

  result=instance;

  //此時還未被初始化的話,在這裡初始化可以保證線程安全

  instance=result=new ThreadSafeDoubleCheckLocking();

  return result;

  上面的雙重鎖檢驗使用了《Effective Java 第2版》提出的一個優化方式,另外值得一提的是,對于 instance 域被聲明為 volatile 是很重要的。當一個變量定義為 volatile 之後,它就具備了兩種特性,第一是保證了此變量對所有線程的可見性,“可見性”指的是當一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的(注意基于volatile變量的運算在并發程式設計下并非是安全的,例如:假設被volatile修飾的域進行自增運算,而自增運算并不是原子操作,那麼第二個線程就可能在讀取舊值和寫回新值的期間讀取到這個域,導緻第二個線程看到的值與第一個線程未自增前的值一樣,詳細了解的話可檢視《深入了解Java虛拟機 第2版》P366 基于volatile型變量的特殊規則);第二是禁止指令重排序優化。

  在進行初始化的時候 instance=result=new

  ThreadSafeDoubleCheckLocking() ,此時 JVM 大緻做了三件事:

  1.給instance配置設定記憶體2.調用構造函數進行初始化3.instance對象指向被配置設定的記憶體

  沒有聲明為 volatile ,那麼指令重排序後,可能執行的順序是 1-3-2,當線程一執行到3這個步驟,還未執行步驟2(instance非null,但未初始化),那麼對于線程二,此時檢測到 instance 并非是 null,直接傳回 instance,就會出現錯誤。需要說明的一點是,JDK 1.5以後, volatile才真正發揮用處,是以在1.5以前,仍然是無法保證安全的,具體可檢視 The "Double-Checked Locking is Broken" Declaration .

  另外一種懶加載方式就是使用靜态内部類的方法:

  public class InitializingOnDemandHolderIdiom {

  private InitializingOnDemandHolderIdiom() {}

  public static InitializingOnDemandHolderIdiom getInstance() {

  return HelperHolder.INSTANCE;

  private static class HelperHolder {

  private static final InitializingOnDemandHolderIdiom INSTANCE=new InitializingOnDemandHolderIdiom();

  這種方式是線程安全的,同時也是懶加載的。 HelperHolder 是私有的,除了 getInstance()外沒有辦法通路。這種方式不需要依賴其他語言特性(volatile,synchronized),也不依賴JDK版本。

  枚舉

  《Effective Java 第2版》P15 中提到實作單例的一種新方式,使用枚舉來實作單例。枚舉類型是Java 5中新增特性的一部分,是以使用這種方式實作的枚舉,要求至少是 JDK 1.5版本及其以上。枚舉本身保證了線程安全,并且提供了序列化機制,是以這種方式寫起來極為簡潔。

  public enum Singleton {

  INSTANCE;

  當然,對于使用枚舉來實作單例模式也有一些缺點,具體可以檢視 StackOverflow 的讨論。

  典型使用場景

  日志紀錄類管理與資料庫的連接配接檔案管理系統