基本介紹
單例模式(Singleton)應該是大家接觸的第一個設計模式,其寫法相較於其他的設計模式來說並不複雜,核心理念也非常簡單:程式從始至終只有同一個該類的例項物件。
舉一個耳熟能詳的例子,比如LOL中的大龍,一場遊戲下來無論如何只有一隻,所以該類只能被例項化一次。再舉一個我們應用程式開發中常見的例子,Spring框架中的Bean作用範圍預設也是單例的。
我相信大家都知道單例的兩種最基本的寫法:餓汗式和懶漢式。但是這兩種寫法都有其弊端所在,除了這兩種寫法外其實還有幾種寫法。此時耳邊彷彿聽到孔乙己的聲音:
“對呀對呀!......回字有四樣寫法,你知道麼?”。
我愈不耐煩了,努著嘴走遠。孔乙己剛用指甲蘸了酒,想在櫃上寫字,見我毫不熱心,便又嘆一口氣,顯出極惋惜的樣子........
大家先彆著急走,回字的四樣寫法沒必要知道,單例的五種寫法還是有必要曉得滴,其他的不說,至少面試的時候還能和麵試官吹下是不,況且這幾種寫法也不是純弔書袋,瞭解過後還是能幫助我們理解其設計思想滴。所以接下來我們們由淺入深,從最容易的寫法開始,一步一步的帶大家掌握單例模式!
寫法介紹
餓漢式
話不多說,先直接上最簡單的寫法,然後我們再慢慢剖析:
public class Signleton01 {
// 私有建構函式,防止別人例項化
private Signleton01(){}
// 靜態屬性,指向一個例項化物件
private static final Signleton01 INSTANCE = new Signleton01();
// 公共方法,以便別人獲取到例項化物件屬性
public static Signleton01 getINSTANCE() {
return INSTANCE;
}
}
單例模式三元素
一個單例模式就這樣寫完了,簡直不要太簡單。 類裡面一共就三個元素:
- 私有建構函式,防止別人例項化
- 靜態屬性,指向一個例項化物件
- 公共方法,以便別人獲取到例項化物件屬性
這三個元素就是單例模式的核心,單例無論哪種寫法,都離不開這三個元素。
這三個元素也很好理解,別人想要用我這個類的例項物件就只能通過我提供的getINSTANCE()
,他想new也new不了第二個物件,自然而然就保證了該類只有唯一物件。我們可以做個試驗,跑100個執行緒同時獲取該類的例項物件,然後列印出物件的hashCode,看看到底是不是獲取的同一個物件:
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Signleton01.getINSTANCE().hashCode());
}).start();
}
}
結果如下:
...
834649078
834649078
834649078
834649078
834649078
...
嗯,全部都是同一個物件。
優缺點
優點:寫法簡單,執行緒安全
缺點:消耗資源,即使程式從沒有用到過該類物件,該類也會初始化一個物件出來
所以為了解決餓汗式的這個缺點, 我們就引出了第二種寫法,懶漢式!
懶漢式
基本寫法
public class Singleton02 {
// 私有建構函式,防止別人例項化
private Singleton02() {}
// 靜態屬性,指向一個例項化物件(注意,這裡沒有例項化物件哦)
private static Singleton02 INSTANCE;
// 公共方法,以便別人獲取到例項化物件屬性
public static Singleton02 getINSTANCE() {
if (INSTANCE == null) {
INSTANCE = new Singleton02();
}
return INSTANCE;
}
}
懶漢式的和餓汗式最大的區別是什麼呢,就是隻有在呼叫getINSTANCE
的時候,才會建立例項,如果你從來沒呼叫過,那麼就不例項化物件。這個就比餓汗式更加節約資源,不過這種寫法並不是懶漢式的完善寫法,它有一個非常大的問題,就是執行緒不同步!我們可以按照之前那種方式建立100個執行緒測試一下結果:
...
1851261656
868907500
988762476
1031371881
593800070
...
可以看到這執行緒一同時拿,拿的都不是同一個物件,這完全就破壞了單例模式。因為很多執行緒在物件沒有初始化前就進入到了if (INSTANCE == null)
判斷語句塊裡,自然而然就會new出不同的物件了。要解決這個執行緒不安全問題,就得上執行緒鎖!
synchronized寫法
public class Singleton02 {
private Singleton02() {}
private static Singleton02 INSTANCE;
// 注意,這裡靜態方法加了synchronized關鍵字
public synchronized static Singleton02 getINSTANCE() {
if (INSTANCE == null) {
INSTANCE = new Singleton02();
}
return INSTANCE;
}
}
當我們在靜態方法加上synchronized
關鍵字後,就可以保證這個方法在同一時間只會有一個執行緒能成功呼叫,也就順理成章的解決了執行緒不安全問題。我們還是測試一下:
...
1226880356
1226880356
1226880356
1226880356
1226880356
...
不管多少個執行緒,拿到的都是同一個物件,達到了單例的要求!
優缺點
懶漢式連基本的執行緒安全都不能保證,就不做討論了,我們這裡主要說的事synchronized
寫法
優點:寫法簡單,節約資源(只有需要該物件的時候才會例項化)
缺點:耗效能
要知道每一次呼叫getINSTANCE()
方法時都會上鎖,這是非常耗效能的。那麼為了解決這個好效能的問題,我們又引申出接下來的一種寫法。
雙重檢測
每一次呼叫getINSTANCE()
方法都會上鎖,這是完全沒有必要的嘛,因為只有物件還沒有例項化的時候我才需要上鎖以保證執行緒安全,物件都例項化了,自然也不用擔心後續的呼叫會new出新的物件。 所以我們這個鎖,可以加在if (INSTANCE == null)
判斷語句塊裡面:
public class Singleton03 {
private Singleton03() {}
private static Singleton03 INSTANCE;
public static Singleton03 getINSTANCE() {
if (INSTANCE == null) {
// 只有在物件還沒有例項化的時候才上鎖
synchronized (Singleton03.class) {
INSTANCE = new Singleton03();
}
}
return INSTANCE;
}
}
這樣就能節約一些效能,但是這樣並沒有做到執行緒安全哦! 因為很多執行緒進入到if (INSTANCE == null)
判斷語句後,雖說是因為鎖不能同時new物件了,但是如果鎖一旦釋放,那麼其他執行緒依然會執行到INSTANCE = new Singleton03()
語句,從而破壞了單例。所以在synchronized
程式碼塊內還要加一層判斷:
public class Singleton03 {
private Singleton03() {}
// 注意,使用雙重檢驗寫法要加上volatile關鍵字,避免指令重排(有個印象就行,這不是本文的重點)
private static volatile Singleton03 INSTANCE;
public static Singleton03 getINSTANCE() {
if (INSTANCE == null) {
// 只有在物件還沒有例項化的時候才上鎖
synchronized (Singleton03.class) {
// 額外加一層判斷
if (INSTANCE == null) {
INSTANCE = new Singleton03();
}
}
}
return INSTANCE;
}
}
synchronized
程式碼塊外面一層判斷,裡面一層判斷,就是有名的雙重檢測(DCL)了!裡面的這一層判斷加了之後呢,第一個執行緒的鎖一旦釋放也不用擔心了,因為此時物件已經例項化,後續的執行緒也執行不了new語句,從而保證了執行緒安全!
優缺點
優點:節約資源(只有需要該物件的時候才會例項化)
缺點:寫法複雜,耗效能(還是上了鎖,還是耗效能)
雖然雙重校驗比synchronized
懶漢式寫法減少了很多鎖效能消耗,但畢竟還是上了鎖,所以為了解決這個鎖效能消耗問題了,又引申出下一種寫法。
內部類
話不多說,直接上程式碼:
public class Singleton04 {
// 老套路,將建構函式私有化
private Singleton04() {}
// 宣告一個內部類,內部類裡持有例項的引用
private static class Inner {
public static final Singleton04 INSTANCE = new Singleton04();
}
// 公共方法
public static Singleton04 getINSTANCE() {
return Inner.INSTANCE;
}
}
這個寫法非常像餓漢式寫法,單例三元素還是那三元素,只不過多加了一個內部類,將例項引用放到內部類裡而已。為啥要這樣寫呢?因為JVM保證了內部類的執行緒安全,即一個內部類在整個程式中不會被重複載入,並且如果你沒有使用到內部類的話,是不會載入這個內部類的。這就非常巧妙的實現了執行緒安全以及節約資源的好處!
優缺點
優點:寫法簡單、節約資源(只有呼叫了getINSTANCE()
方法才會載入內部類,才會例項化物件)、執行緒安全(JVM保證了內部類的執行緒安全)
缺點:會被序列化或者反射破壞單例
這個缺點可以說是吹毛求疵,因為之前所有寫法都會被序列化、反射破壞單例。雖然說是吹毛求疵,但我們們搞技術的還是得做到了解全部細節,我來演示一下怎樣破壞這個單例
通過反射破壞單例
public static void main(String[] args) throws Exception {
// 建立100個執行緒同時訪問例項
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Singleton04.getINSTANCE().hashCode());
}).start();
}
// 反射破壞單例
Class<Singleton04> clazz = Singleton04.class;
// 拿到無參建構函式並將其設定為可訪問,無視private
Constructor<Singleton04> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
// 建立物件
Singleton04 singleton04 = constructor.newInstance();
System.out.println("反射:" + singleton04.hashCode());
}
執行結果如下:
...
2115147268
2115147268
反射:1078694789
2115147268
2115147268
...
如果是通過正常的訪問例項方法,是完全可以做到單例的要求,但是如果用反射的形式來建立一個物件,則就破壞了單例,一個程式中就出現了多個不同的例項物件。那麼為了解決這個吹毛求疵的問題,聰明的前輩們想到了一個完美的寫法!
列舉
// 注意,這裡是列舉
public enum Singleton05 {
// 例項
INSTANCE;
// 公共方法
public static Singleton05 getINSTANCE() {
return INSTANCE;
}
}
哎嘿,不是說所有單例都是那三元素嗎,這裡怎麼只有兩個元素呀!這是因為列舉就沒有構造方法,自然而然就做到了私有化建構函式的效果,而且比私有化建構函式效果更好!因為都沒有建構函式了,連序列化和反射都破壞不了這種寫法的單例!!
眼見為實,我們做個試驗:
public static void main(String[] args) throws Exception {
// 建立100個執行緒同時訪問例項
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Singleton05.getINSTANCE().hashCode());
}).start();
}
// 反射破壞單例
Class<Singleton05> clazz = Singleton05.class;
// 拿到無參建構函式並將其設定為可訪問,無視private
Constructor<Singleton05> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
// 建立物件
Singleton05 singleton05 = constructor.newInstance();
System.out.println("反射:" + singleton05.hashCode());
}
執行結果如下:
...
422057313
422057313
422057313
422057313
Exception in thread "main" java.lang.NoSuchMethodException: Singleton05.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
當執行到反射那一塊程式碼的時候,程式直接報錯,原因就是我之前所說的一樣,列舉沒有構造方法,你自然就無法通過反射來建立物件了!
優缺點
此方法乃是最完美的方法,真是佩服想出這種寫法的前輩!
總結
五個寫法全部介紹完畢,每個寫法都有其特點,根據自己的需求來寫就好了!每種寫法理解其特點後,寫出來也就非常輕鬆。就像我一開始說的一樣,理解這五種寫法也不是弔書袋,每一種寫法都有其背後的思考,有些寫法思路真的讓人歎服,至少我瞭解到內部類和列舉寫法的時候我心裡就是:我靠!這都能想出來,太牛逼了吧......
好的程式碼就是藝術作品,希望我們都能碼出好的藝術出來!