單例模式(Singleton)看了就懂

單例,故名思議,一個只能建立一個實例的類。java

單例被普遍應用於Spring的bean(默認)、線程池、數據庫鏈接池、緩存,還有其餘一些無狀態的類如servlet。程序員

一個不必多例的類實現了單例能夠節約空間(顯而易見),節省資源(線程、數據庫鏈接)。數據庫

單例模式有這麼多好處,那咱們來實現它吧,首先想到的是建立一個對象要使用new方法,new方法調用的是類的構造函數,想要不被程序員隨意的new對象能夠將類的構造函數設爲私有,而後再提供一個獲取這個類實例的方法,因此就有了下面這個實現。緩存

一、只能正確運行在單線程模式下的單例實現:安全

 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文件後未必是一行字節碼,就算是一行字節碼,在解釋執行或即時編譯執行轉化成機器碼時也可能對應多條指令,以上結論原理不在本文介紹範圍以內。spa

思考:java裏有一個很方便的實現線程安全的synchronized修飾符,加上不就實現線程安全了嗎?是的,下面這個實現就是在getInstance方法上簡單加上synchronized修飾符。線程

二、對性能不敏感的多線程安全單例實現:

 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都將是額外的負擔。

 

思考:能不能只在第一次調用對象實例須要建立的時候才同步代碼,其餘時候不一樣步代碼的方法呢?有的。

三、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也是已經被初始化過的。

思考:這個寫法看上去很難理解,我想到一個實現方式,既然是靜態變量類型,直接在變量聲明時賦值,或者寫一個靜態塊初始化不就行了,靜態類型的變量初始化是伴隨着類加載進行的,不也是線程安全的嗎?確實。

四、利用類加載器實現的急切加載單例:

 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),那就沒法保證單例了。

思考:怎樣避免類的實例只有在真正須要它的時候才被初始化呢? 能夠用私有靜態內部類實現。

五、利用類加載器實現的懶加載單例:

 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(而且遵循雙親委託機制),不然單例會失效。

相關文章
相關標籤/搜索