趣說單例模式——選班長

Java團長_發表於2019-01-26

來源:程式設計師私房菜(ID:eson_15)


注:本文人物形象均為原創,人物姓名均為虛構。


“碼農大學”是“互聯省”的一所名牌大學,學習氣氛濃厚,不管是學校的環境還是學生綜合素質,都非常高。開學的第一天,同學們都興致勃勃,這不,一起來看下設計模式的課堂裡。


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg


自我介紹完之後,老師開始進入本節課的主題了。


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg


提出這個問題後,大家開始相互討論起來。


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg


1. 懶漢式單例


於是小夏開始實現這個班長類:首先,我們要在班長類中將構造方法私有化,這樣是防止在其他地方被例項化,就出現多個班長物件了。然後我們在班長類中自己 new 一個班長物件出來。最後給外界提供一個方法,返回這個班長物件即可。如下(程式碼可以左右滑動):


public class Monitor {
   private static Monitor monitor = null;
   private Monitor() {}
   public static Monitor getMonitor() {
       if (monitor == null) {
           monitor = new Monitor();
       }
       return monitor;
   }
}


640?wx_fmt=jpeg

640?wx_fmt=jpeg

小美開始了他的分析:我覺得小夏的程式碼還是不能保證一個班長例項的,因為存線上程安全問題。假如執行緒A執行到了monitor = new Monitor();,此時班長物件還沒建立,執行緒B執行到判斷 monitor == null時,條件為true,於是也進入到if裡面去執行monitor = new Monitor();了,這樣記憶體中就出現了兩個班長例項了。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg


於是,小美根據自己的思路,將小夏的程式碼做了修改,在獲取班長物件的方法上面加了個 synchronized 關鍵字,這樣就能解決執行緒安全問題了。


public static synchronized Monitor getMonitor() {
   if (monitor == null) {
       monitor = new Monitor();
   }
   return monitor;
}


640?wx_fmt=jpeg

小夏覺得這種修改不太好,於是和小美討論起來:小美,你這樣改雖然可以解決執行緒安全問題,但是效率太差了,不管班長物件有沒有被建立好,後面每個執行緒併發走到這,可想而知,都做了無用的等待呀。

640?wx_fmt=jpeg

還沒等小美說話,小劉舉起手來,他想到了更好的解決方案:老師,我有更好的辦法!我們不能在方法上新增 synchronized關鍵字,但可以在方法內部新增。比如:


public static Monitor getMonitor() {
   if (monitor == null) {
       synchronized (Monitor.class) {
           if (monitor == null) {
               monitor = new Monitor();
           }
       }
   }
   return monitor;
}


640?wx_fmt=jpeg

640?wx_fmt=jpeg

小劉開始給小夏解釋到:這判斷是有目的的,第一層判斷如果 monitor 例項不為空,那皆大歡喜,說明物件已經被建立過了,直接返回該物件即可,不會走到 synchronized 部分,所以班長物件被建立了之後,不會影響到效能。


第二層判斷是在 synchronized 程式碼塊裡面,為什麼要再做一次判斷呢?假如 monitor 物件是 null,那麼第一層判斷後,肯定有很多執行緒已經進來第一層了,那麼即使在第二層某個執行緒執行完了之後,釋放了鎖,其他執行緒還會進入 synchronized 程式碼塊,如果不判斷,那麼又會被建立一次,這就導致了多個班長物件的建立。所以第二層起到了一個防範作用。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

在同學們踴躍發言和討論之後,老師做了一下簡短的總結:同學們都分析的很棒,這就是“懶漢式”單例模式,為什麼稱為“懶漢式”呢?顧名思義,就是一開始不建立,等到需要的時候再去建立物件。


小劉的這個“懶漢式”單例模式已經寫的很不錯了,不過這裡還有一個問題,雖然可能已經超出了本課程的要求了,但是我還是來補充一下,在定義班長物件時,要加一個 volatile 關鍵字。即:


private static volatile Monitor monitor = null;


640?wx_fmt=jpeg

640?wx_fmt=jpeg

於是,老師開始和同學們分析:我們先看下 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. 餓漢式單例


看到大家一直在激烈的討論問題,小帥一直在座位上思考……終於他也發言了。

640?wx_fmt=jpeg

小帥一邊說一邊寫起了程式碼:


public class Monitor {
   private static Monitor monitor = new Monitor ();
   private  Monitor () {}
   public static Monitor getMonitor() {
       return monitor;
   }
}


小帥繼續說到,在定義的時候就將班長物件建立出來,這樣還沒有執行緒安全問題。

640?wx_fmt=jpeg

老師正要講“餓漢式”單利模式,剛好小帥說出來了,於是就借題發揮:小帥的這種方式就叫做“餓漢式”單例模式,顧名思義,一開始就建立出來,比較“飢餓”,這種方式是不存線上程安全問題的。這個“餓漢式”單利相對來說比較簡單,也很好理解,我就不多說了。


3. 單例模式的擴充套件


聽了小帥的發言,小夏開始納悶了,他開始和旁邊的小劉討論起來,老師好像看出來了小夏有疑惑,於是……

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

老師藉著這個問題,繼續講課:我們要知道,萬物存在即合理,但是也不是十全十美的,不管是“懶漢式”還是“餓漢式”,都有它們各自的優缺點以及使用場景。


針對剛剛小夏提到的問題,“餓漢式”雖然簡單粗暴,而且執行緒安全,但是它不是延遲載入的,也就是說類建立的時候,就必須要把這個班長例項建立好,而不是在需要的時候才建立,這是第一點。


我再舉個例子,也許更能說明問題:假如在獲取班長物件的時候,需要傳一個引數進去呢?也就是說,我在選班長的時候有個要求,比如我想選一個身高高於175cm的人做班長,那麼我在獲取班長例項物件時,需要傳一個身高引數,該方法就應該這樣設計:


public static Monitor getMonitor(Long height) {……}


針對這種情況,“餓漢式”就不行了,就得用“懶漢式”單例了。

640?wx_fmt=jpeg

640?wx_fmt=jpeg


3.1 靜態內部類


老師看了看手錶,離下課還有16分鐘,於是還想再講點東西。

640?wx_fmt=jpeg

於是老師又提出了個問題給同學們:班長這個物件有個屬性是不會變的,那就是他所在的班級,所以班級可以直接定義好,老師翻到了PPT的下一頁,如:


public class Monitor {
   public static String CLASS_INFO = "通訊工程(1)班";
   private static Monitor monitor = new Monitor ();
   private Monitor () {}
   public static Monitor getMonitor() {
       return monitor;
   }
}


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

老師解釋到:是可以獲取,但是這樣獲取的話,因為都是static修飾的,呼叫Monitor.CLASS_INFO時,也會執行構造方法將monitor物件初始化,但是我現在不想初始化班長物件(因為會影響效能),我只想要獲取他的班級資訊。

640?wx_fmt=jpeg

640?wx_fmt=jpeg


於是老師把繼續把 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;
   }
}


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

小美好像發現了新大陸,非常興奮:我還發現了一個特點,使用靜態內部類這種方式,也是實現懶載入的,也就是說當我們呼叫 getInstance 方法的時候,才會去初始化班長物件,這和“懶漢式”是一樣的效果;而且在內部類中,初始化這個班長物件的時候,是直接 new 出來的,這個和“餓漢式”很像。哇,難道這就是兩種方式的結合體嗎?

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

3.2 列舉單例


老師意猶未盡,但看了看錶,還有4分鐘就下課了,感覺講不完了,於是最後給同學們丟擲一種方式,讓同學們下課後自己研究研究。

640?wx_fmt=jpeg


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg


於是老師把PPT又往後翻了一頁:


public enum Monitor {
   INSTANCE;
   // 其他任意方法
}


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

老師見同學們激情澎湃,於是決定把這個講完:上面這段列舉程式碼比較抽象,我說具體點,我們就舉前面提到的例子,比如班長有個屬性是所屬班級,那麼我現在要建立這樣一個班長例項,我可以這麼寫:


public enum Monitor {
   INSTANCE("通訊工程(1)班");
   private String classInfo;
   EnumSingleton(String classInfo) {
       this.classInfo = classInfo;
   }
   // 省略get set方法
}


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg


於是老師繼續往下講:當你們工作之後,實際場景肯定不像課堂上說的這麼簡單,就像小劉說的那樣,如果有很多屬性呢?而且屬性可以改變該怎麼做呢?這時候,我們可以藉助列舉類來實現單例,為什麼說“藉助”呢?我先建立一個班長物件,裡面是屬性(這裡我就用一個屬性代表一下,你們可以認為有很多屬性),如下:


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 物件。


640?wx_fmt=jpeg


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg


就這樣,老師被幾個學生架到生活區的小飯館了,當然咯,最後還少不了買單……


(完)


Java團長

專注於Java乾貨分享

640?wx_fmt=jpeg

掃描上方二維碼獲取更多Java乾貨

相關文章