單例模式有幾種寫法?
糾結單例模式有幾種寫法有用嗎?有點用,面試中經常選擇其中一種或幾種寫法作為話頭,考查設計模式和coding style的同時,還很容易擴充套件到其他問題。
這裡講解幾種筆者常用的寫法,但切忌生搬硬套,去記“茴香豆的寫法”。程式設計最大的樂趣在於“know everything, control everything”。
JDK版本:oracle java 1.8.0_102
大體可分為4類,下面分別介紹他們的基本形式、變種及特點。
飽漢模式
飽漢是變種最多的單例模式。我們從飽漢出發,透過其變種逐漸瞭解實現單例模式時需要關注的問題。
基礎的飽漢
飽漢,即已經吃飽,不著急再吃,餓的時候再吃。所以他就先不初始化單例,等第一次使用的時候再初始化,即“懶載入”。
// 飽漢
// UnThreadSafe
public class Singleton1 {
private static Singleton1 singleton = null;
private Singleton1() {
}
public static Singleton1 getInstance() {
if (singleton == null) {
singleton = new Singleton1();
}
return singleton;
}
}
飽漢模式的核心就是懶載入。好處是更啟動速度快、節省資源,一直到例項被第一次訪問,才需要初始化單例;小壞處是寫起來麻煩,大壞處是執行緒不安全,if語句存在競態條件。
寫起來麻煩不是大問題,可讀性好啊。因此,單執行緒環境下,基礎飽漢是筆者最喜歡的寫法。但多執行緒環境下,基礎飽漢就徹底不可用了。下面的幾種變種都在試圖解決基礎飽漢執行緒不安全的問題。
飽漢 - 變種 1
最粗暴的犯法是用synchronized關鍵字修飾getInstance()方法,這樣能達到絕對的執行緒安全。
// 飽漢
// ThreadSafe
public class Singleton1_1 {
private static Singleton1_1 singleton = null;
private Singleton1_1() {
}
public synchronized static Singleton1_1 getInstance() {
if (singleton == null) {
singleton = new Singleton1_1();
}
return singleton;
}
}
變種1的好處是寫起來簡單,且絕對執行緒安全;壞處是併發效能極差,事實上完全退化到了序列。單例只需要初始化一次,但就算初始化以後,synchronized的鎖也無法避開,從而getInstance()完全變成了序列操作。效能不敏感的場景建議使用。
飽漢 - 變種 2
變種2是“臭名昭著”的DCL 1.0。
針對變種1中單例初始化後鎖仍然無法避開的問題,變種2在變種1的外層又套了一層check,加上synchronized內層的check,即所謂“雙重檢查鎖”(Double Check Lock,簡稱DCL)。
// 飽漢
// UnThreadSafe
public class Singleton1_2 {
private static Singleton1_2 singleton = null;
public int f1 = 1; // 觸發部分初始化問題
public int f2 = 2;
private Singleton1_2() {
}
public static Singleton1_2 getInstance() {
// may get half object
if (singleton == null) {
synchronized (Singleton1_2.class) {
if (singleton == null) {
singleton = new Singleton1_2();
}
}
}
return singleton;
}
}
變種2的核心是DCL,看起來變種2似乎已經達到了理想的效果:懶載入+執行緒安全。可惜的是,正如註釋中所說,DCL仍然是執行緒不安全的,由於指令重排序,你可能會得到“半個物件”,即”部分初始化“問題。詳細在看完變種3後,可參考下面這篇文章,這裡不再贅述。
關鍵字的作用、原理/
飽漢 - 變種 3
變種3專門針對變種2,可謂DCL 2.0。
針對變種3的“半個物件”問題,變種3在instance上增加了volatile關鍵字,原理見上述參考。
// 飽漢
// ThreadSafe
public class Singleton1_3 {
private static volatile Singleton1_3 singleton = null;
public int f1 = 1; // 觸發部分初始化問題
public int f2 = 2;
private Singleton1_3() {
}
public static Singleton1_3 getInstance() {
if (singleton == null) {
synchronized (Singleton1_3.class) {
// must be a complete instance
if (singleton == null) {
singleton = new Singleton1_3();
}
}
}
return singleton;
}
}
多執行緒環境下,變種3更適用於效能敏感的場景。但後面我們將瞭解到,就算是執行緒安全的,還有一些辦法能破壞單例。
當然,還有很多方式,能透過與volatile類似的方式防止部分初始化。讀者可自行閱讀記憶體屏障相關內容,但面試時不建議主動裝逼。
餓漢模式
與飽漢相對,餓漢很餓,只想著儘早吃到。所以他就在最早的時機,即類載入時初始化單例,以後訪問時直接返回即可。
// 餓漢
// ThreadSafe
public class Singleton2 {
private static final Singleton2 singleton = new Singleton2();
private Singleton2() {
}
public static Singleton2 getInstance() {
return singleton;
}
}
餓漢的好處是天生的執行緒安全(得益於類載入機制),寫起來超級簡單,使用時沒有延遲;壞處是有可能造成資源浪費(如果類載入後就一直不使用單例的話)。
值得注意的時,單執行緒環境下,餓漢與飽漢在效能上沒什麼差別;但多執行緒環境下,由於飽漢需要加鎖,餓漢的效能反而更優。
Holder模式
我們既希望利用餓漢模式中靜態變數的方便和執行緒安全;又希望透過懶載入規避資源浪費。Holder模式滿足了這兩點要求:核心仍然是靜態變數,足夠方便和執行緒安全;透過靜態的Holder類持有真正例項,間接實現了懶載入。
// Holder模式
// ThreadSafe
public class Singleton3 {
private static class SingletonHolder {
private static final Singleton3 singleton = new Singleton3();
private SingletonHolder() {
}
}
private Singleton3() {
}
/**
* 勘誤:多寫了個synchronized。。
public synchronized static Singleton3 getInstance() {
return SingletonHolder.singleton;
}
*/
public static Singleton3 getInstance() {
return SingletonHolder.singleton;
}
}
相對於餓漢模式,Holder模式僅增加了一個靜態內部類的成本,與飽漢的變種3效果相當(略優),都是比較受歡迎的實現方式。同樣建議考慮。
列舉模式
用列舉實現單例模式,相當好用,但可讀性是不存在的。
基礎的列舉
將列舉的靜態成員變數作為單例的例項:
// 列舉
// ThreadSafe
public enum Singleton4 {
SINGLETON;
}
程式碼量比餓漢模式更少。但使用者只能直接訪問例項Singleton4.SINGLETON——事實上,這樣的訪問方式作為單例使用也是恰當的,只是犧牲了靜態工廠方法的優點,如無法實現懶載入。
醜陋但好用的語法糖
Java的列舉是一個“醜陋但好用的語法糖”。
列舉型單例模式的本質
透過反編譯開啟語法糖,就看到了列舉型別的本質,簡化如下:
// 列舉
// ThreadSafe
public class Singleton4 extends Enum {
...
public static final Singleton4 SINGLETON = new Singleton4();
...
}
本質上和餓漢模式相同,區別僅在於公有的靜態成員變數。
用列舉實現一些trick
這一部分與單例沒什麼關係,可以跳過。如果選擇閱讀也請認清這樣的事實:雖然列舉相當靈活,但如何恰當的使用列舉有一定難度。一個足夠簡單的典型例子是TimeUnit類,建議有時間耐心閱讀。
上面已經看到,列舉型單例的本質仍然是一個普通的類。實際上,我們可以在列舉型型單例上增加任何普通類可以完成的功能。要點在於列舉例項的初始化,可以理解為例項化了一個匿名內部類。為了更明顯,我們在Singleton4_1中定義一個普通的私有成員變數,一個普通的公有成員方法,和一個公有的抽象成員方法,如下:
// 列舉
// ThreadSafe
public enum Singleton4_1 {
SINGLETON("enum is the easiest singleton pattern, but not the most readable") {
public void testAbsMethod() {
print();
System.out.println("enum is ugly, but so flexible to make lots of trick");
}
};
private String comment = null;
Singleton4_1(String comment) {
this.comment = comment;
}
public void print() {
System.out.println("comment=" + comment);
}
abstract public void testAbsMethod();
public static Singleton4_1 getInstance() {
return SINGLETON;
}
}
這樣,列舉類Singleton4_1中的每一個列舉例項不僅繼承了父類Singleton4_1的成員方法print(),還必須實現父類Singleton4_1的抽象成員方法testAbsMethod()。
總結
上面的分析都忽略了反射和序列化的問題。透過反射或序列化,我們仍然能夠訪問到私有構造器,建立新的例項破壞單例模式。此時,只有列舉模式能天然防範這一問題。反射和序列化筆者還不太瞭解,但基本原理並不難,可以在其他模式上手動實現。
下面繼續忽略反射和序列化的問題,做個總結回味一下:
作者公眾號:一起寫程式
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3244/viewspace-2797898/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 面試中單例模式有幾種寫法?面試單例模式
- 孔乙己的疑問:單例模式有幾種寫法單例模式
- 單例模式你會幾種寫法?單例模式
- 面試突擊50:單例模式有幾種寫法?面試單例模式
- 單例模式(下)---聊一聊單例模式的幾種寫法單例模式
- 單例模式(下) - 聊一聊單例模式的幾種寫法單例模式
- 單例模式(下) – 聊一聊單例模式的幾種寫法單例模式
- 單例的幾種寫法單例
- 單例模式的五種寫法單例模式
- 單例模式的六種寫法單例模式
- 單例模式的七種寫法單例模式
- Java:單例模式的七種寫法Java單例模式
- 回字有四種寫法,那你知道單例有五種寫法嗎單例
- 設計模式 - 單例模式Singleton的8種寫法設計模式單例
- 單例模式的七種寫法,你都知道嗎?單例模式
- 單例模式的正確寫法單例模式
- Python單例的一種簡單寫法Python單例
- 八、目前JDK中,單例模式這3種寫法你知道嗎?JDK單例模式
- 實現單例模式的 9 種方法,你知道幾種?單例模式
- Java基礎系列-單例的7種寫法Java單例
- cache 有幾種寫法,你都會了麼?
- 設計模式學習(一)單例模式的幾種實現方式設計模式單例
- Singleton——單例模式(8種)單例模式
- 你知道JavaScript的繼承有幾種寫法嗎?JavaScript繼承
- 論單例的寫法單例
- 單例模式的幾種實現And反射對其的破壞單例模式反射
- Vim常見模式有幾種?模式
- 短視訊系統原始碼,幾種常見的單例模式原始碼單例模式
- Android中單例模式的幾個坑Android單例模式
- Kotlin下的5種單例模式Kotlin單例模式
- 整理下java六種單例模式Java單例模式
- 單例模式:5種實現方式單例模式
- 單例模式的各種實現單例模式
- 面試之---手寫單例模式面試單例模式
- Python中的單例模式的幾種實現方式的及優化Python單例模式優化
- Java併發程式設計中的設計模式解析(二)一個單例的七種寫法Java程式設計設計模式單例
- [原] Json 陣列之茴香豆的茴有幾種寫法JSON陣列
- java23種設計模式—— 二、單例模式Java設計模式單例