意圖
保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。
別名:單件模式
單例模式的誕生
【開發】:老大,爲什麼我保存配置信息,每次都和我預期的不一樣啊,總是會覆蓋?
【BOSS】:哈?我來看看。
【BOSS】:你每次使用的時候都會new一個新的配置對象嗎?
【開發】:對啊,有什麼問題?
【BOSS】:那肯定不對啊,像這種配置信息,全局只應該有一個,不然會互相影響!
HeadFirst 核心代碼
餓漢型 (不推薦)
public class HazardousTypeSingleton {
private static final App APP = new App();
// 私有構造方法
private HazardousTypeSingleton () {}
// 類加載時已初始化,不會有多線程的問題
static App getInstance() {
System.out.println("APP - 餓漢型模式");
return APP;
}
}
名字由來:因爲隨着類加載而加載,顯得很“急迫”,所以稱之爲餓漢型
**評價:**這樣的寫法和全局變量沒有本質的區別,不推薦
懶漢型 (不推薦)
public class LazyTypeSingleton {
private LazyTypeSingleton () {}
// 靜態私用成員,沒有初始化
private static App intance = null;
/***
* 直接加synchronized關鍵字
*/
synchronized static App getIntance () {
System.out.println("APP - 懶漢型模式");
if (null == intance) {
intance = new App();
return intance;
}
return intance;
}
}
名字由來:調用時才加載,因此稱之爲懶漢型
**評價:**這樣寫有延遲加載的功能,但是加了一個synchronized大鎖,因此多線程環境下效率較低
懶漢型之雙重鎖校驗 🧡
public class LazyTypeSingleton {
// volatile關鍵字修飾,防止指令重排
private volatile static App app = null;
/***
* Double Check Lock(DCL) 雙重鎖校驗
*/
static App getInstanceByDCL () {
if (null == app) {
synchronized (LazyTypeSingleton.class) {
if (null == app) {
System.out.println("APP - 餓漢模式DCL 雙重鎖校驗");
app = new App();
return app;
}
}
}
return app;
}
}
注意volatile關鍵字起到的作用,詳情請見:https://juejin.im/post/5ebadd9df265da7bda414c20
**評價:**比較推薦的寫法,可以保證線程安全,同時具備延時加載的效果
靜態內部類方式🧡
public class InnterTypeSingleton {
private InnterTypeSingleton(){
throw new IllegalStateException();
}
// 靜態內部類方式,類似餓漢保證天然的線程安全
private static class SingletonHolder{
private final static App app = new App();
}
static App getInstance(){
System.out.println("APP - 靜態內部類方式(Holder)");
return SingletonHolder.app;
}
}
**評價:**線程安全,調用效率高,可以延時加載
靜態內部類之神奇的報錯
public class InnterTypeSingletonError {
private InnterTypeSingletonError(){
System.out.println(5 / 0);
}
private static class SingletonHolder{
private final static InnterTypeSingletonError app = new InnterTypeSingletonError();
}
static InnterTypeSingletonError getInstance(){
System.out.println("APP - 靜態內部類方式(Holder)");
return SingletonHolder.app;
}
public static void main(String[] args){
try {
InnterTypeSingletonError.getInstance();
} catch (Throwable t) {
t.printStackTrace();
}
try {
InnterTypeSingletonError.getInstance();
} catch (Throwable t) {
t.printStackTrace();
}
}
}
注意看上文中代碼塊:
private InnterTypeSingletonError(){
System.out.println(5 / 0);
}
這樣一定會出錯,運行結果報錯信息如下:
APP - 靜態內部類方式(Holder)
APP - 靜態內部類方式(Holder)
java.lang.ExceptionInInitializerError
at com.design.singleton.InnterTypeSingletonError.getInstance(InnterTypeSingletonError.java:23)
at com.design.singleton.InnterTypeSingletonError.main(InnterTypeSingletonError.java:28)
Caused by: java.lang.ArithmeticException: / by zero
at com.design.singleton.InnterTypeSingletonError.<init>(InnterTypeSingletonError.java:14)
at com.design.singleton.InnterTypeSingletonError.<init>(InnterTypeSingletonError.java:11)
at com.design.singleton.InnterTypeSingletonError$SingletonHolder.<clinit>(InnterTypeSingletonError.java:18)
... 2 more
java.lang.NoClassDefFoundError: Could not initialize class com.design.singleton.InnterTypeSingletonError$SingletonHolder
at com.design.singleton.InnterTypeSingletonError.getInstance(InnterTypeSingletonError.java:23)
at com.design.singleton.InnterTypeSingletonError.main(InnterTypeSingletonError.java:34)
可以發現它第一次報錯是正常的異常,第二次如果再報錯就是Could not initialize class ,爲什麼呢?
因爲:類加載時靜態變量只會在第一次加載時,進行初始化,此後不管成不成功,都不會進行第二次初始化了
所以使用的時候需要注意
枚舉方式🧡
public enum EnumSingleton {
/***
* APP對象
*/
APP;
private App app;
EnumSingleton() {
app = new App();
}
public App getInstance() {
System.out.println("**************************");
System.out.println("APP - 枚舉方式");
return app;
}
}
**評價:**線程安全,調用效率高,不能延時加載,可以天然的防止反射和反序列化調用
什麼場景適用
在以下情況可以使用單例模式:
- 當類只能有一個實例而且客戶可以從一個衆所周知的訪問點訪問它時
- 當這個唯一實例應該是通過子類化可擴展的,並且客戶應該無需更改代碼就能使用一個擴展的實例時
Code/生活中的實際應用
在很多項目中的數據庫連接池,亦或是配置中心,配置文件對象等等,非常常見~
總結
感謝Java3Y的文章:三歪寫Bug寫哭了,從中學習到了內部類使用時的神器報錯
單例模式使用的場景其實固化,任何需要單一對象工作時的場景都可以使用單例模式,同時只推薦以下三種寫法:
- 基於雙重鎖校驗的懶漢型
- 靜態內部類方式
- 枚舉方式
相關代碼鏈接
- 兼顧了《HeadFirst》以及《GOF》兩本經典書籍中的案例
- 提供了友好的閱讀指導