06.14 單例模式 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;
if (result == null) {
//此時還未被初始化的話,在這裡初始化可以保證線程安全
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 的討論。

典型使用場景

  • 日誌紀錄類
  • 管理與數據庫的連接
  • 文件管理系統


分享到:


相關文章: