Java設計模式-單例

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/8860649


寫軟件的時候經常需要用到打印日誌功能,可以幫助你調試和定位問題,項目上線後還可以幫助你分析數據。但是Java原生帶有的System.out.println()方法卻很少在真正的項目開發中使用,甚至像findbugs等代碼檢查工具還會認爲使用System.out.println()是一個bug。


爲什麼作爲Java新手神器的System.out.println(),到了真正項目開發當中會被唾棄呢?其實只要細細分析,你就會發現它的很多弊端。比如不可控制,所有的日誌都會在項目上線後照常打印,從而降低運行效率;又或者不能將日誌記錄到本地文件,一旦打印被清除,日誌將再也找不回來;再或者打印的內容沒有Tag區分,你將很難辨別這一行日誌是在哪個類裏打印的。


你的leader也不是傻瓜,用System.out.println()的各項弊端他也清清楚楚,因此他今天給你的任務就是製作一個日誌工具類,來提供更好的日誌功能。不過你的leader人還不錯,並沒讓你一開始就實現一個具備各項功能的牛逼日誌工具類,只需要一個能夠控制打印級別的日誌工具就好。


這個需求對你來說並不難,你立刻就開始動手編寫了,並很快完成了第一個版本:

[java] view plain copy
  1. public class LogUtil {  
  2.   
  3.     public final int DEGUB = 0;  
  4.   
  5.     public final int INFO = 1;  
  6.   
  7.     public final int ERROR = 2;  
  8.   
  9.     public final int NOTHING = 3;  
  10.   
  11.     public int level = DEGUB;  
  12.   
  13.     public void debug(String msg) {  
  14.         if (DEGUB >= level) {  
  15.             System.out.println(msg);  
  16.         }  
  17.     }  
  18.   
  19.     public void info(String msg) {  
  20.         if (INFO >= level) {  
  21.             System.out.println(msg);  
  22.         }  
  23.     }  
  24.   
  25.     public void error(String msg) {  
  26.         if (ERROR >= level) {  
  27.             System.out.println(msg);  
  28.         }  
  29.     }  
  30.   
  31. }  
通過這個類來打印日誌,只需要控制level的級別,就可以自由地控制打印的內容。比如現在項目處於開發階段,就將level設置爲DEBUG,這樣所有的日誌信息都會被打印。而項目如果上線了,可以把level設置爲INFO,這樣就只能看到INFO及以上級別的日誌打印。如果你只想看到錯誤日誌,就可以把level設置爲ERROR。而如果你開發的項目是客戶端版本,不想讓任何日誌打印出來,可以將level設置爲NOTHING。打印的時候只需要調用:
[java] view plain copy
  1. new LogUtil().debug("Hello World");  

你迫不及待地將這個工具介紹給你的leader,你的leader聽完你的介紹後說:“好樣的,今後大夥都用你寫的這個工具來打印日誌了!”


可是沒過多久,你的leader找到你來反饋問題了。他說雖然這個工具好用,可是打印這種事情是不區分對象的,這裏每次需要打印日誌的時候都需要new出一個新的LogUtil,太佔用內存了,希望你可以將這個工具改成用單例模式實現。


你認爲你的leader說的很有道理,而且你也正想趁這個機會練習使用一下設計模式,於是你寫出瞭如下的代碼:

[java] view plain copy
  1. public class LogUtil {  
  2.   
  3.     private static LogUtil sLogUtil;  
  4.   
  5.     public final int DEGUB = 0;  
  6.   
  7.     public final int INFO = 1;  
  8.   
  9.     public final int ERROR = 2;  
  10.   
  11.     public final int NOTHING = 3;  
  12.   
  13.     public int level = DEGUB;  
  14.   
  15.     private LogUtil() {  
  16.     }  
  17.   
  18.     public static LogUtil getInstance() {  
  19.         if (sLogUtil == null) {  
  20.             sLogUtil = new LogUtil();  
  21.         }  
  22.         return sLogUtil;  
  23.     }  
  24.   
  25.     public void debug(String msg) {  
  26.         if (DEGUB >= level) {  
  27.             System.out.println(msg);  
  28.         }  
  29.     }  
  30.   
  31.     public void info(String msg) {  
  32.         if (INFO >= level) {  
  33.             System.out.println(msg);  
  34.         }  
  35.     }  
  36.   
  37.     public void error(String msg) {  
  38.         if (ERROR >= level) {  
  39.             System.out.println(msg);  
  40.         }  
  41.     }  
  42.   
  43. }  
首先將LogUtil的構造函數私有化,這樣就無法使用new關鍵字來創建LogUtil的實例了。然後使用一個sLogUtil私有靜態變量來保存實例,並提供一個公有的getInstance方法用於獲取LogUtil的實例,在這個方法裏面判斷如果sLogUtil爲空,就new出一個新的LogUtil實例,否則就直接返回sLogUtil。這樣就可以保證內存當中只會存在一個LogUtil的實例了。單例模式完工!這時打印日誌的代碼需要改成如下方式:
[java] view plain copy
  1. LogUtil.getInstance().debug("Hello World");  
你將這個版本展示給你的leader瞧,他看後笑了笑,說:“雖然這看似是實現了單例模式,可是還存在着bug的哦。


你滿腹狐疑,單例模式不都是這樣實現的嗎?還會有什麼bug呢?


你的leader提示你,使用單例模式就是爲了讓這個類在內存中只能有一個實例的,可是你有考慮到在多線程中打印日誌的情況嗎?如下面代碼所示:

[java] view plain copy
  1. public static LogUtil getInstance() {  
  2.     if (sLogUtil == null) {  
  3.         sLogUtil = new LogUtil();  
  4.     }  
  5.     return sLogUtil;  
  6. }  

如果現在有兩個線程同時在執行getInstance方法,第一個線程剛執行完第2行,還沒執行第3行,這個時候第二個線程執行到了第2行,它會發現sLogUtil還是null,於是進入到了if判斷裏面。這樣你的單例模式就失敗了,因爲創建了兩個不同的實例。


你恍然大悟,不過你的思維非常快,立刻就想到了解決辦法,只需要給方法加上同步鎖就可以了,代碼如下:

[java] view plain copy
  1. public synchronized static LogUtil getInstance() {  
  2.     if (sLogUtil == null) {  
  3.         sLogUtil = new LogUtil();  
  4.     }  
  5.     return sLogUtil;  
  6. }  

這樣,同一時刻只允許有一個線程在執行getInstance裏面的代碼,這樣就有效地解決了上面會創建兩個實例的情況。


你的leader看了你的新代碼後說:“恩,不錯。這確實解決了有可能創建兩個實例的情況,但是這段代碼還是有問題的。”


你緊張了起來,怎麼還會有問題啊?


你的leader笑笑:“不用緊張,這次不是bug,只是性能上可以優化一些。你看一下,如果是在getInstance方法上加了一個synchronized,那麼我每次去執行getInstace方法的時候都會受到同步鎖的影響,這樣運行的效率會降低,其實只需要在第一次創建LogUtil實例的時候加上同步鎖就好了。我來教你一下怎麼把它優化的更好。”


首先將synchronized關鍵字從方法聲明中去除,把它加入到方法體當中:

[java] view plain copy
  1. public static LogUtil getInstance() {  
  2.     synchronized (LogUtil.class) {  
  3.         if (sLogUtil == null) {  
  4.             sLogUtil = new LogUtil();  
  5.         }  
  6.         return sLogUtil;  
  7.     }  
  8. }  

這樣效果是和直接在方法上加synchronized完全一致的。然後在synchronized的外面再加一層判斷,如下所示:

[java] view plain copy
  1. public static LogUtil getInstance() {  
  2.     if (sLogUtil == null) {  
  3.         synchronized (LogUtil.class) {  
  4.             if (sLogUtil == null) {  
  5.                 sLogUtil = new LogUtil();  
  6.             }  
  7.         }  
  8.     }  
  9.     return sLogUtil;  
  10. }  

代碼改成這樣之後,只有在sLogUtil還沒被初始化的時候纔會進入到第3行,然後加上同步鎖。等sLogUtil一但初始化完成了,就再也走不到第3行了,這樣執行getInstance方法也不會再受到同步鎖的影響,效率上會有一定的提升。


你情不自禁讚歎到,這方法真巧妙啊,能想得出來實在是太聰明瞭。


你的leader馬上謙虛起來:“這種方法叫做雙重鎖定(Double-Check Locking),可不是我想出來的,更多的資料你可以在網上查一查。”


單例:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。 

發佈了12 篇原創文章 · 獲贊 2 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章