單例,故名思議,一個只能創建一個實例的類。 單例被廣泛應用於Spring的bean(預設)、線程池、資料庫連接池、緩存,還有其他一些無狀態的類如servlet。 一個沒必要多例的類實現了單例可以節約空間(顯而易見),節省資源(線程、資料庫連接)。 單例模式有這麼多好處,那我們來實現它吧,首先想到的是 ...
單例,故名思議,一個只能創建一個實例的類。
單例被廣泛應用於Spring的bean(預設)、線程池、資料庫連接池、緩存,還有其他一些無狀態的類如servlet。
一個沒必要多例的類實現了單例可以節約空間(顯而易見),節省資源(線程、資料庫連接)。
單例模式有這麼多好處,那我們來實現它吧,首先想到的是創建一個對象要使用new方法,new方法調用的是類的構造函數,想要不被程式員隨意的new對象可以將類的構造函數設為私有,然後再提供一個獲取這個類實例的方法,所以就有了下麵這個實現。
1、只能正確運行在單線程模式下的單例實現:
1 public class SingletonSingleThread { 2 3 private static SingletonSingleThread instance; 4 5 private SingletonSingleThread(){} 6 7 public static SingletonSingleThread getInstance(){ 8 if(instance == null){ 9 instance = new SingletonSingleThread(); 10 } 11 return instance; 12 } 13 }
以上代碼只能保證在單線程下運行,在多線程環境下可能有多個線程在第8行時判斷到instance當前是指向null,然後都去執行第9行的代碼。
如果讀者瞭解過volatile修飾變數的作用,可能想到將以上代碼修改成如下形式,因為volatile可以保證線程之間變數的“可見性”,就可以保證每個線程在第8行判斷的時候都是instance的最新引用了?
1 public class SingletonSingleThread { 2 3 private static volatile SingletonSingleThread instance; 4 5 private SingletonSingleThread(){} 6 7 public static SingletonSingleThread getInstance(){ 8 if(instance == null){ 9 instance = new SingletonSingleThread(); 10 } 11 return instance; 12 } 13 }
但是第8行和第9行是兩行代碼,並不是原子操作,完全可能出現線程A執行通過第8行校驗,準備執行第9行的時候,另一個線程B來到第8行校驗,也通過了校驗。
實際上就算是代碼中的一行指令也不是原子操作,在編譯成.class文件後未必是一行位元組碼,就算是一行位元組碼,在解釋執行或即時編譯執行轉化成機器碼時也可能對應多條指令,以上結論原理不在本文介紹範圍之內。
思考:java里有一個很方便的實現線程安全的synchronized修飾符,加上不就實現線程安全了嗎?是的,下麵這個實現就是在getInstance方法上簡單加上synchronized修飾符。
2、對性能不敏感的多線程安全單例實現:
1 public class SingletonSlow { 2 3 private static SingletonSlow instance; 4 5 private SingletonSlow(){} 6 7 public synchronized static SingletonSlow getInstance(){ 8 if(instance == null){ 9 instance = new SingletonSlow(); 10 } 11 return instance; 12 } 13 }
如果程式對一個單例實現的getInstance()方法效率不敏感可以使用這種實現方式。好處就是直觀,簡單。壞處也顯而易見,只有當SingletonSlow對象第一次被創建時是需要同步的,之後的調用synchronized都將是額外的負擔。
思考:能不能只在第一次調用對象實例需要創建的時候才同步代碼,其他時候不同步代碼的方法呢?有的。
3、DCL單例模式(雙重鎖檢查) 註意:這種方式只能應用於JDK1.5及以後的版本,JDK1.5解決了volatile無法實現雙重鎖單例的bug:
1 public class SingletonDCL { 2 3 private volatile static SingletonDCL instance; 4 5 private SingletonDCL(){} 6 7 public static SingletonDCL getInstance(){ 8 if(instance == null){ 9 synchronized(SingletonDCL.class){ 10 if(instance == null){ 11 instance = new SingletonDCL(); 12 } 13 } 14 } 15 return instance; 16 } 17 }
DCL方式雖然也用到了同步保證單例,但是它的效率要遠遠高於第2種實現方式。首先實例變數instance用volatile修飾,保證第8行拿到的是instance的最新引用,這行判斷可以快速逃避instance已經被初始化的情況,當然這行代碼還是會出現多個線程都判斷為真的情況,第9行的代碼保證了10-12行代碼只有一個線程能執行,第10行重新判斷instance引用為空的意義在於解決以下情況的發生:
①若線程A和B同時通過第8行代碼判斷,等待進入同步塊依次執行,若A先執行,則B獲得執行時間時instance已經被線程A初始化了。
②若線程A通過第8行代碼判斷,進入同步塊開始執行11行代碼未執行完時,線程B執行到第8行判斷通過,這時A同步代碼塊執行完畢,B雖然接下來不需要等待直接進入同步塊,但instance也是已經被初始化過的。
思考:這個寫法看上去很難理解,我想到一個實現方式,既然是靜態變數類型,直接在變數聲明時賦值,或者寫一個靜態塊初始化不就好了,靜態類型的變數初始化是伴隨著類載入進行的,不也是線程安全的嗎?確實。
4、利用類載入器實現的急切載入單例:
1 public class SingletonEagerly { 2 3 private static SingletonEagerly instance = new SingletonEagerly(); 4 5 private SingletonEagerly(){} 6 7 public static SingletonEagerly getInstance(){ 8 return instance; 9 } 10 }
為什麼叫急切載入單例呢,因為依賴於java的類載入機制,類被載入的時機未必是在需要使用類的對象時,也許在還不需要這個類的實例的時候類的實例就已經被初始化了,如果這個單例很耗費資源,我們肯定想採用懶載入的方式去實現他。
這種實現還有一個缺陷就是依賴於當前線程的ClassLoader(),如果類被多個ClassLoader載入(比如servlet),那就無法保證單例了。
思考:怎樣避免類的實例只有在真正需要它的時候才被初始化呢? 可以用私有靜態內部類實現。
5、利用類載入器實現的懶載入單例:
1 public class SingletonLazyByClassLoader { 2 3 private SingletonLazyByClassLoader(){} 4 5 public static SingletonLazyByClassLoader getInstance(){ 6 return SingletonHolder.instance; 7 } 8 9 private static class SingletonHolder{ 10 private static final SingletonLazyByClassLoader instance= new SingletonLazyByClassLoader(); 11 } 12 }
將類的唯一實例放在內部靜態類里,這樣外部只要不調用getInstance方法就不會有類載入器載入去載入內部靜態類,而內部靜態類是私有的,所以只有第一次instance方法被調用的時候才會初始化了類的實例變數。
遺憾的是這種實現方式仍然無法避免ClassLoader不一樣的問題。
如果實在對getInstance效率無要求,使用方式2實現單例最簡單直觀;
如果程式運行在Jdk1.5及以上的環境,又不會覺得實現麻煩,強烈推薦使用方式3實現單例,高效、可靠;
如果嫌方式3麻煩或者運行在Jdk1.5以前的版本,則根據是否對懶載入有需求使用方式5或方式4,同時要保證程式中用到的類載入器是AppClassLoader或先代載入器是AppClassLoader,否則單例會失效。