趣說單例模式——選班長
來源:程式設計師私房菜(ID:eson_15)
注:本文人物形象均為原創,人物姓名均為虛構。
“碼農大學”是“互聯省”的一所名牌大學,學習氣氛濃厚,不管是學校的環境還是學生綜合素質,都非常高。開學的第一天,同學們都興致勃勃,這不,一起來看下設計模式的課堂裡。
自我介紹完之後,老師開始進入本節課的主題了。
提出這個問題後,大家開始相互討論起來。
1. 懶漢式單例
於是小夏開始實現這個班長類:首先,我們要在班長類中將構造方法私有化,這樣是防止在其他地方被例項化,就出現多個班長物件了。然後我們在班長類中自己 new 一個班長物件出來。最後給外界提供一個方法,返回這個班長物件即可。如下(程式碼可以左右滑動):
public class Monitor {
private static Monitor monitor = null;
private Monitor() {}
public static Monitor getMonitor() {
if (monitor == null) {
monitor = new Monitor();
}
return monitor;
}
}
小美開始了他的分析:我覺得小夏的程式碼還是不能保證一個班長例項的,因為存線上程安全問題。假如執行緒A執行到了monitor = new Monitor();,此時班長物件還沒建立,執行緒B執行到判斷 monitor == null時,條件為true,於是也進入到if裡面去執行monitor = new Monitor();了,這樣記憶體中就出現了兩個班長例項了。
於是,小美根據自己的思路,將小夏的程式碼做了修改,在獲取班長物件的方法上面加了個 synchronized 關鍵字,這樣就能解決執行緒安全問題了。
public static synchronized Monitor getMonitor() {
if (monitor == null) {
monitor = new Monitor();
}
return monitor;
}
小夏覺得這種修改不太好,於是和小美討論起來:小美,你這樣改雖然可以解決執行緒安全問題,但是效率太差了,不管班長物件有沒有被建立好,後面每個執行緒併發走到這,可想而知,都做了無用的等待呀。
還沒等小美說話,小劉舉起手來,他想到了更好的解決方案:老師,我有更好的辦法!我們不能在方法上新增 synchronized關鍵字,但可以在方法內部新增。比如:
public static Monitor getMonitor() {
if (monitor == null) {
synchronized (Monitor.class) {
if (monitor == null) {
monitor = new Monitor();
}
}
}
return monitor;
}
小劉開始給小夏解釋到:這判斷是有目的的,第一層判斷如果 monitor 例項不為空,那皆大歡喜,說明物件已經被建立過了,直接返回該物件即可,不會走到 synchronized 部分,所以班長物件被建立了之後,不會影響到效能。
第二層判斷是在 synchronized 程式碼塊裡面,為什麼要再做一次判斷呢?假如 monitor 物件是 null,那麼第一層判斷後,肯定有很多執行緒已經進來第一層了,那麼即使在第二層某個執行緒執行完了之後,釋放了鎖,其他執行緒還會進入 synchronized 程式碼塊,如果不判斷,那麼又會被建立一次,這就導致了多個班長物件的建立。所以第二層起到了一個防範作用。
在同學們踴躍發言和討論之後,老師做了一下簡短的總結:同學們都分析的很棒,這就是“懶漢式”單例模式,為什麼稱為“懶漢式”呢?顧名思義,就是一開始不建立,等到需要的時候再去建立物件。
小劉的這個“懶漢式”單例模式已經寫的很不錯了,不過這裡還有一個問題,雖然可能已經超出了本課程的要求了,但是我還是來補充一下,在定義班長物件時,要加一個 volatile 關鍵字。即:
private static volatile Monitor monitor = null;
於是,老師開始和同學們分析:我們先看下 monitor = new Monitor();,在這個操作中,JVM主要乾了三件事:
1、在堆空間裡分配一部分空間;
2、執行 Monitor 的構造方法進行初始化;
3、把 monitor 物件指向在堆空間裡分配好的空間。
把第3步執行完,這個 monitor 物件就已經不為空了。
但是,當我們編譯的時候,編譯器在生成彙編程式碼的時候會對流程順序進行優化。優化的結果不是我們可以控制的,有可能是按照1、2、3的順序執行,也有可能按照1、3、2的順序執行。
如果是按照1、3、2的順序執行,恰巧在執行到3的時候(還沒執行2),突然跑來了一個執行緒,進來 getMonitor() 方法之後判斷 monitor 不為空就返回了 monitor 例項。此時 monitor 例項雖不為空,但它還沒執行構造方法進行初始化(即沒有執行2),所以該執行緒如果對那些需要初始化的引數進行操作那就悲劇了。但是加了 volatile 關鍵字的話,就不會出現這個問題。這是由 volatitle 本身的特性決定的。
關於 volatile 的更多知識已經超出了本課程的範圍了,感興趣的同學可以課後自己研究研究。
2. 餓漢式單例
看到大家一直在激烈的討論問題,小帥一直在座位上思考……終於他也發言了。
小帥一邊說一邊寫起了程式碼:
public class Monitor {
private static Monitor monitor = new Monitor ();
private Monitor () {}
public static Monitor getMonitor() {
return monitor;
}
}
小帥繼續說到,在定義的時候就將班長物件建立出來,這樣還沒有執行緒安全問題。
老師正要講“餓漢式”單利模式,剛好小帥說出來了,於是就借題發揮:小帥的這種方式就叫做“餓漢式”單例模式,顧名思義,一開始就建立出來,比較“飢餓”,這種方式是不存線上程安全問題的。這個“餓漢式”單利相對來說比較簡單,也很好理解,我就不多說了。
3. 單例模式的擴充套件
聽了小帥的發言,小夏開始納悶了,他開始和旁邊的小劉討論起來,老師好像看出來了小夏有疑惑,於是……
老師藉著這個問題,繼續講課:我們要知道,萬物存在即合理,但是也不是十全十美的,不管是“懶漢式”還是“餓漢式”,都有它們各自的優缺點以及使用場景。
針對剛剛小夏提到的問題,“餓漢式”雖然簡單粗暴,而且執行緒安全,但是它不是延遲載入的,也就是說類建立的時候,就必須要把這個班長例項建立好,而不是在需要的時候才建立,這是第一點。
我再舉個例子,也許更能說明問題:假如在獲取班長物件的時候,需要傳一個引數進去呢?也就是說,我在選班長的時候有個要求,比如我想選一個身高高於175cm的人做班長,那麼我在獲取班長例項物件時,需要傳一個身高引數,該方法就應該這樣設計:
public static Monitor getMonitor(Long height) {……}
針對這種情況,“餓漢式”就不行了,就得用“懶漢式”單例了。
3.1 靜態內部類
老師看了看手錶,離下課還有16分鐘,於是還想再講點東西。
於是老師又提出了個問題給同學們:班長這個物件有個屬性是不會變的,那就是他所在的班級,所以班級可以直接定義好,老師翻到了PPT的下一頁,如:
public class Monitor {
public static String CLASS_INFO = "通訊工程(1)班";
private static Monitor monitor = new Monitor ();
private Monitor () {}
public static Monitor getMonitor() {
return monitor;
}
}
老師解釋到:是可以獲取,但是這樣獲取的話,因為都是static修飾的,呼叫Monitor.CLASS_INFO時,也會執行構造方法將monitor物件初始化,但是我現在不想初始化班長物件(因為會影響效能),我只想要獲取他的班級資訊。
於是老師把繼續把 PPT 翻到了下一頁:
public class Monitor {
public static String CLASS_INFO = "通訊工程(1)班";
/**
* 靜態內部類,用來建立班長物件
*/
private static class MonitorCreator {
private static Monitor monitor = new Monitor();
}
private Monitor() {}
public static Monitor getInstance() {
return MonitorCreator.monitor;
}
}
小美好像發現了新大陸,非常興奮:我還發現了一個特點,使用靜態內部類這種方式,也是實現懶載入的,也就是說當我們呼叫 getInstance 方法的時候,才會去初始化班長物件,這和“懶漢式”是一樣的效果;而且在內部類中,初始化這個班長物件的時候,是直接 new 出來的,這個和“餓漢式”很像。哇,難道這就是兩種方式的結合體嗎?
3.2 列舉單例
老師意猶未盡,但看了看錶,還有4分鐘就下課了,感覺講不完了,於是最後給同學們丟擲一種方式,讓同學們下課後自己研究研究。
於是老師把PPT又往後翻了一頁:
public enum Monitor {
INSTANCE;
// 其他任意方法
}
老師見同學們激情澎湃,於是決定把這個講完:上面這段列舉程式碼比較抽象,我說具體點,我們就舉前面提到的例子,比如班長有個屬性是所屬班級,那麼我現在要建立這樣一個班長例項,我可以這麼寫:
public enum Monitor {
INSTANCE("通訊工程(1)班");
private String classInfo;
EnumSingleton(String classInfo) {
this.classInfo = classInfo;
}
// 省略get set方法
}
於是老師繼續往下講:當你們工作之後,實際場景肯定不像課堂上說的這麼簡單,就像小劉說的那樣,如果有很多屬性呢?而且屬性可以改變該怎麼做呢?這時候,我們可以藉助列舉類來實現單例,為什麼說“藉助”呢?我先建立一個班長物件,裡面是屬性(這裡我就用一個屬性代表一下,你們可以認為有很多屬性),如下:
public class Monitor {
private String classInfo;
private Monitor() {}
//省略get set方法
}
接下來,我就要“藉助”列舉,創造出班長這個單例實體,而且支援屬性可修改,大家請看PPT:
public enum EnumSingleton {
INSTANCE;
private Monitor monitor;
EnumSingleton() {
monitor = new Monitor();
}
public Monitor getMonitor() {
return monitor;
}
}
老師對著PPT講到:Monitor 類就是我們的班長類,我放到私有構造方法中初始化了,然後列舉類中同樣提供一個 getMonitor 方法給外界提供這個班長物件,模式和前面講的單例差不多。我們可以通過 EnumSingleton.INSTANCE.getMonitor(); 即可獲取到 monitor 物件。
就這樣,老師被幾個學生架到生活區的小飯館了,當然咯,最後還少不了買單……
(完)
Java團長
專注於Java乾貨分享
掃描上方二維碼獲取更多Java乾貨
相關文章
- 嘻哈說:設計模式之單例模式設計模式單例
- 說說你對單例模式的理解?如何實現?單例模式
- 單例模式單例模式
- 創造模式 單例模式模式單例
- 建立型模式:單例模式模式單例
- 設計模式(單例模式)設計模式單例
- [設計模式] 單例模式設計模式單例
- 設計模式-單例模式設計模式單例
- 設計模式 —— 單例模式設計模式單例
- 設計模式 單例模式設計模式單例
- 設計模式——單例模式設計模式單例
- 單例模式解析單例模式
- python 單例模式Python單例模式
- java 單例模式Java單例模式
- 單例模式(Singleton)單例模式
- php單例模式PHP單例模式
- 單例模式(3)單例模式
- Java單例模式Java單例模式
- 單例模式 singleton單例模式
- 單例模式(SingletonPattern)單例模式
- python單例模式Python單例模式
- 您的單例模式,真的單例嗎?單例模式
- 單例模式,真不簡單單例模式
- 設計模式學習(一)單例模式補充——單例模式析構設計模式單例
- 設計模式-單例模式、多例模式設計模式單例
- 設計模式(一)_單例模式設計模式單例
- 常用設計模式-單例模式設計模式單例
- 設計模式之單例模式設計模式單例
- Java設計模式【單例模式】Java設計模式單例
- Java設計模式 | 單例模式Java設計模式單例
- 001設計模式:單例模式設計模式單例
- # Python設計模式 單例模式Python設計模式單例
- 設計模式一(單例模式)設計模式單例
- 設計模式之☞單例模式設計模式單例
- Java設計模式——單例模式Java設計模式單例
- Java設計模式–單例模式Java設計模式單例
- js設計模式--單例模式JS設計模式單例
- Java設計模式-單例模式Java設計模式單例