Bill Pugh 單例實現
1. 概述
在本教程中,我們將討論 Bill Pugh Singleton 實作。單例模式有多種實作。值得注意的是,延遲載入單例和急切載入單例實作是最突出的。此外,它們還支援同步和非同步版本。
Bill Pugh Singleton 實作支援延遲載入的單例物件。在接下來的部分中,我們將探討其實現,並了解它如何解決其他實現所面臨的挑戰。
2. 單例實現的基本原理
Singleton 是一種創意的設計模式。顧名思義,這種設計模式有助於建立類別的單一實例。該實例在整個應用程式中使用。它通常用於創建昂貴且耗時的類,例如連接工廠、REST 適配器、Dao 等類。
在繼續之前,我們先來看看Java類別的單例實作的基本原理:
- 私有建構函式以防止使用
new
運算子進行實例化 - 最好使用名稱
getInstance()
的公共靜態方法來傳回該類別的單一實例 - 用於儲存類別的唯一實例的私人靜態變數
此外,在多執行緒環境中限制類別的單一實例並推遲實例的初始化直到它被引用可能存在挑戰。因此,這就是一種實現比其他實現得分更高的地方。牢記這些挑戰,我們將看到 Bill Pugh Singleton 實現如何成為獲勝者。
3. Bill Pugh 單例實現
大多數情況下,單例實作面臨以下一項或兩項挑戰:
- 預先載入
- 同步帶來的開銷
Bill Pugh 或 Holder Singleton 模式在私人靜態內部類別的幫助下解決了這兩個問題:
public class BillPughSingleton {
private BillPughSingleton() {
}
private static class SingletonHelper {
private static final BillPughSingleton BILL_PUGH_SINGLETON_INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.BILL_PUGH_SINGLETON_INSTANCE;
}
}
Java 應用程式中的類別載入器僅將靜態內部類別SingletonHelper
載入到記憶體中一次,即使多個執行緒呼叫getInstance()
也是如此。值得注意的是,我們也沒有使用synchronized
。這消除了存取同步方法時鎖定和解鎖物件的開銷。因此,這種方法解決了其他實作所面臨的缺陷。
現在,讓我們看看它是如何運作的:
@Test
void givenSynchronizedLazyLoadedImpl_whenCallgetInstance_thenReturnSingleton() {
Set<BillPughSingleton> setHoldingSingletonObj = new HashSet<>();
List<Future<BillPughSingleton>> futures = new ArrayList<>();
ExecutorService executorService = Executors.newFixedThreadPool(10);
Callable<BillPughSingleton> runnableTask = () -> {
logger.info("run called for:" + Thread.currentThread().getName());
return BillPughSingleton.getInstance();
};
int count = 0;
while(count < 10) {
futures.add(executorService.submit(runnableTask));
count++;
}
futures.forEach(e -> {
try {
setHoldingSingletonObj.add(e.get());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
});
executorService.shutdown();
assertEquals(1, setHoldingSingletonObj.size());
}
在上面的方法中,多個執行緒同時呼叫getInstance()
。但是,它始終傳回相同的物件參考。
4. Bill Pugh 與非同步實現
讓我們為單線程和多線程環境實作單例模式。我們將避免在多執行緒環境中使用synchronized
關鍵字來實現。
4.1.延遲載入單例實現
基於前面所描述的基本原理,我們來實作一個單例類別:
public class LazyLoadedSingleton {
private static LazyLoadedSingleton lazyLoadedSingletonObj;
private LazyLoadedSingleton() {
}
public static LazyLoadedSingleton getInstance() {
if (null == lazyLoadedSingletonObj) {
lazyLoadedSingletonObj = new LazyLoadedSingleton();
}
return lazyLoadedSingletonObj;
}
}
僅當呼叫getInstance()
方法時才會建立LazyLoadedSingleton
物件。這稱為延遲初始化。但是,當多個執行緒並發呼叫getInstance()
方法時,由於髒讀,此操作會失敗。即使沒有使用synchronized.
讓我們看看LazyLoadedSingleton
類別是否只建立一個物件:
@Test
void givenLazyLoadedImpl_whenCallGetInstance_thenReturnSingleInstance() throws ClassNotFoundException {
Class bs = Class.forName("com.baledung.billpugh.LazyLoadedSingleton");
assertThrows(IllegalAccessException.class, () -> bs.getDeclaredConstructor().newInstance());
LazyLoadedSingleton lazyLoadedSingletonObj1 = LazyLoadedSingleton.getInstance();
LazyLoadedSingleton lazyLoadedSingletonObj2 = LazyLoadedSingleton.getInstance();
assertEquals(lazyLoadedSingletonObj1.hashCode(), lazyLoadedSingletonObj2.hashCode());
}
上述方法嘗試借助反射 API 並呼叫getInstance()
方法來實例化LazyLoadedSingleton
。但是,實例化會因反射而失敗,並且getInstance()
總是傳回LazyLoadedSingleton
類別的單一實例。
4.2.熱切加載的單例實現
前面部分討論的實作僅適用於單線程環境。然而,對於多執行緒環境,我們可以考慮使用類別級靜態變數的不同方法:
public class EagerLoadedSingleton {
private static final EagerLoadedSingleton EAGER_LOADED_SINGLETON = new EagerLoadedSingleton();
private EagerLoadedSingleton() {
}
public static EagerLoadedSingleton getInstance() {
return EAGER_LOADED_SINGLETON;
}
}
類別層級變數EAGER_LOADED_SINGLETON
是靜態的。因此,當應用程式啟動時,即使不需要它,也會立即載入它。然而,如前所述,Bill Pugh 的實作支援單執行緒和多執行緒環境中的延遲載入。
讓我們來看看EagerLoadedSingleton
類別的實際應用:
@Test
void givenEagerLoadedImpl_whenCallgetInstance_thenReturnSingleton() {
Set<EagerLoadedSingleton> set = new HashSet<>();
List<Future<EagerLoadedSingleton>> futures = new ArrayList<>();
ExecutorService executorService = Executors.newFixedThreadPool(10);
Callable<EagerLoadedSingleton> runnableTask = () -> {
return EagerLoadedSingleton.getInstance();
};
int count = 0;
while(count < 10) {
futures.add(executorService.submit(runnableTask));
count++;
}
futures.forEach(e -> {
try {
set.add(e.get());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
});
executorService.shutdown();
assertEquals(1, set.size());
}
上述方法中,多個執行緒呼叫runnableTask.
然後, run()
方法呼叫getInstance()
來取得EagerLoadedSingleton
的實例。但是,每次getInstance()
都會傳回該物件的單一實例。
上面的程式碼在多線程環境中工作,但它表現出急切加載,這顯然是一個缺點。
5. Bill Pugh 與同步單例實現
之前,我們在單線程環境中看到了LazyLoadedSingleton
。讓我們修改它以支援多執行緒環境中的單例模式:
public class SynchronizedLazyLoadedSingleton {
private static SynchronizedLazyLoadedSingleton synchronizedLazyLoadedSingleton;
private SynchronizedLazyLoadedSingleton() {
}
public static synchronized SynchronizedLazyLoadedSingleton getInstance() {
if (null == synchronizedLazyLoadedSingleton) {
synchronizedLazyLoadedSingleton = new SynchronizedLazyLoadedSingleton();
}
return synchronizedLazyLoadedSingleton;
}
}
有趣的是,透過在方法getInstance()
上使用synchronized
關鍵字,我們限制執行緒同時存取它。我們可以使用雙重檢查鎖定方法來實現性能更高的變體。
然而,Bill Pugh 的實作顯然是贏家,因為它可以在多執行緒環境中使用,而無需同步開銷。
讓我們確認這是否適用於多執行緒環境:
@Test
void givenSynchronizedLazyLoadedImpl_whenCallgetInstance_thenReturnSingleton() {
Set<SynchronizedLazyLoadedSingleton> setHoldingSingletonObj = new HashSet<>();
List<Future<SynchronizedLazyLoadedSingleton>> futures = new ArrayList<>();
ExecutorService executorService = Executors.newFixedThreadPool(10);
Callable<SynchronizedLazyLoadedSingleton> runnableTask = () -> {
logger.info("run called for:" + Thread.currentThread().getName());
return SynchronizedLazyLoadedSingleton.getInstance();
};
int count = 0;
while(count < 10) {
futures.add(executorService.submit(runnableTask));
count++;
}
futures.forEach(e -> {
try {
setHoldingSingletonObj.add(e.get());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
});
executorService.shutdown();
assertEquals(1, setHoldingSingletonObj.size());
}
就像EagerLoadedSingleton
一樣, SynchronizedLazyLoadedSingleton
類別也在多執行緒設定中傳回單一物件。但這次程式以惰性方式載入單例物件。然而,由於同步,它會帶來開銷。
六,結論
在本文中,我們將 Bill Pugh 單例實作與其他流行的單例實作進行了比較。 Bill Pugh Singleton 的實作效能更好並且支援延遲載入。因此,許多應用程式和圖書館廣泛使用它。
與往常一樣,本文中使用的程式碼可以在 GitHub 上找到。