深刻java單例模式

在GoF的23種設計模式中,單例模式是比較簡單的一種。然而,有時候越是簡單的東西越容易出現問題。下面就單例設計模式詳細的探討一下。
 
所謂單例模式,簡單來講,就是在整個應用中保證只有一個類的實例存在。就像是Java Web中的application,也就是提供了一個全局變量,用處至關普遍,好比保存全局數據,實現全局性的操做等。
 
1. 最簡單的實現
 
首先,可以想到的最簡單的實現是,把類的構造函數寫成private的,從而保證別的類不能實例化此類,而後在類中提供一個靜態的實例並可以返回給使用者。這樣,使用者就能夠經過這個引用使用到這個類的實例了。
 
public  class SingletonClass { 

   private  static  final SingletonClass instance =  new SingletonClass(); 
    
   public  static SingletonClass getInstance() { 
     return instance; 
  } 
    
   private SingletonClass() { 
     
  } 
    
}
 
如上例,外部使用者若是須要使用SingletonClass的實例,只能經過getInstance()方法,而且它的構造方法是private的,這樣就保證了只能有一個對象存在。
 
2. 性能優化——lazy loaded
 
上面的代碼雖然簡單,可是有一個問題——不管這個類是否被使用,都會建立一個instance對象。若是這個建立過程很耗時,好比須要鏈接10000次數據庫(誇張了…:-)),而且這個類還並不必定會被使用,那麼這個建立過程就是無用的。怎麼辦呢?
 
爲了解決這個問題,咱們想到了新的解決方案:
 
public  class SingletonClass { 

   private  static SingletonClass instance =  null
    
   public  static SingletonClass getInstance() { 
     if(instance ==  null) { 
      instance =  new SingletonClass(); 
    } 
     return instance; 
  } 
    
   private SingletonClass() { 
     
  } 
    
}
 
代碼的變化有兩處——首先,把instance初始化爲null,直到第一次使用的時候經過判斷是否爲null來建立對象。由於建立過程不在聲明處,因此那個final的修飾必須去掉。
 
咱們來想象一下這個過程。要使用SingletonClass,調用getInstance()方法。第一次的時候發現instance是null,而後就新建一個對象,返回出去;第二次再使用的時候,由於這個instance是static的,因此已經不是null了,所以不會再建立對象,直接將其返回。
 
這個過程就成爲lazy loaded,也就是遲加載——直到使用的時候才進行加載。
 
3. 同步
 
上面的代碼很清楚,也很簡單。然而就像那句名言:「80%的錯誤都是由20%代碼優化引發的」。單線程下,這段代碼沒有什麼問題,但是若是是多線程,麻煩就來了。咱們來分析一下:
 
線程A但願使用SingletonClass,調用getInstance()方法。由於是第一次調用,A就發現instance是null的,因而它開始建立實例,就在這個時候,CPU發生時間片切換,線程B開始執行,它要使用SingletonClass,調用getInstance()方法,一樣檢測到instance是null——注意,這是在A檢測完以後切換的,也就是說A並無來得及建立對象——所以B開始建立。B建立完成後,切換到A繼續執行,由於它已經檢測完了,因此A不會再檢測一遍,它會直接建立對象。這樣,線程A和B各自擁有一個SingletonClass的對象——單例失敗!
 
解決的方法也很簡單,那就是加鎖:
 
public  class SingletonClass { 

   private  static SingletonClass instance =  null
    
   public  synchronized  static SingletonClass getInstance() { 
     if(instance ==  null) { 
      instance =  new SingletonClass(); 
    } 
     return instance; 
  } 
    
   private SingletonClass() { 
     
  } 
    
}
 
是要getInstance()加上同步鎖,一個線程必須等待另一個線程建立完成後才能使用這個方法,這就保證了單例的惟一性。
 
4. 又是性能
 
上面的代碼又是很清楚很簡單的,然而,簡單的東西每每不夠理想。這段代碼毫無疑問存在性能的問題——synchronized修飾的同步塊但是要比通常的代碼段慢上幾倍的!若是存在不少次getInstance()的調用,那性能問題就不得不考慮了!
 
讓咱們來分析一下,到底是整個方法都必須加鎖,仍是僅僅其中某一句加鎖就足夠了?咱們爲何要加鎖呢?分析一下出現lazy loaded的那種情形的緣由。緣由就是檢測null的操做和建立對象的操做分離了。若是這兩個操做可以原子地進行,那麼單例就已經保證了。因而,咱們開始修改代碼:
 
public  class SingletonClass { 

   private  static SingletonClass instance =  null
    
   public  static SingletonClass getInstance() { 
     synchronized (SingletonClass. class) { 
       if(instance ==  null) { 
        instance =  new SingletonClass(); 
      } 
    }     
     return instance; 
  } 
    
   private SingletonClass() { 
     
  } 
    
}
 
首先去掉getInstance()的同步操做,而後把同步鎖加載if語句上。可是這樣的修改起不到任何做用:由於每次調用getInstance()的時候必然要同步,性能問題仍是存在。若是……若是咱們事先判斷一下是否是爲null再去同步呢?
 
public  class SingletonClass { 

   private  static SingletonClass instance =  null

   public  static SingletonClass getInstance() { 
     if (instance ==  null) { 
       synchronized (SingletonClass. class) { 
         if (instance ==  null) { 
          instance =  new SingletonClass(); 
        } 
      } 
    } 
     return instance; 
  } 

   private SingletonClass() { 

  } 

}
 
還有問題嗎?首先判斷instance是否是爲null,若是爲null,加鎖初始化;若是不爲null,直接返回instance。
 
這就是double-checked locking設計實現單例模式。到此爲止,一切都很完美。咱們用一種很聰明的方式實現了單例模式。
 
5. 從源頭檢查
 
下面咱們開始說編譯原理。所謂編譯,就是把源代碼「翻譯」成目標代碼——大多數是指機器代碼——的過程。針對Java,它的目標代碼不是本地機器代碼,而是虛擬機代碼。編譯原理裏面有一個很重要的內容是編譯器優化。所謂編譯器優化是指,在不改變原來語義的狀況下,經過調整語句順序,來讓程序運行的更快。這個過程成爲reorder。
 
要知道,JVM只是一個標準,並非實現。JVM中並無規定有關編譯器優化的內容,也就是說,JVM實現能夠自由的進行編譯器優化。
 
下面來想一下,建立一個變量須要哪些步驟呢?一個是申請一塊內存,調用構造方法進行初始化操做,另外一個是分配一個指針指向這塊內存。這兩個操做誰在前誰在後呢?JVM規範並無規定。那麼就存在這麼一種狀況,JVM是先開闢出一塊內存,而後把指針指向這塊內存,最後調用構造方法進行初始化。
 
下面咱們來考慮這麼一種狀況:線程A開始建立SingletonClass的實例,此時線程B調用了getInstance()方法,首先判斷instance是否爲null。按照咱們上面所說的內存模型,A已經把instance指向了那塊內存,只是尚未調用構造方法,所以B檢測到instance不爲null,因而直接把instance返回了——問題出現了,儘管instance不爲null,但它並無構造完成,就像一套房子已經給了你鑰匙,但你並不能住進去,由於裏面尚未收拾。此時,若是B在A將instance構造完成以前就是用了這個實例,程序就會出現錯誤了!
 
因而,咱們想到了下面的代碼:
 
public  class SingletonClass { 

   private  static SingletonClass instance =  null

   public  static SingletonClass getInstance() { 
     if (instance ==  null) { 
      SingletonClass sc; 
       synchronized (SingletonClass. class) { 
        sc = instance; 
         if (sc ==  null) { 
           synchronized (SingletonClass. class) { 
             if(sc ==  null) { 
              sc =  new SingletonClass(); 
            } 
          } 
          instance = sc; 
        } 
      } 
    } 
     return instance; 
  } 

   private SingletonClass() { 

  } 
    
}
 
咱們在第一個同步塊裏面建立一個臨時變量,而後使用這個臨時變量進行對象的建立,而且在最後把instance指針臨時變量的內存空間。寫出這種代碼基於如下思想,即synchronized會起到一個代碼屏蔽的做用,同步塊裏面的代碼和外部的代碼沒有聯繫。所以,在外部的同步塊裏面對臨時變量sc進行操做並不影響instance,因此外部類在instance=sc;以前檢測instance的時候,結果instance依然是null。
 
不過,這種想法徹底是 錯誤的!同步塊的釋放保證在此以前——也就是同步塊裏面——的操做必須完成,可是並不保證同步塊以後的操做不能因編譯器優化而調換到同步塊結束以前進行。所以,編譯器徹底能夠把instance=sc;這句移到內部同步塊裏面執行。這樣,程序又是錯誤的了!
 
6. 解決方案
 
說了這麼多,難道單例沒有辦法在Java中實現嗎?其實否則!
 
在JDK 5以後,Java使用了新的內存模型。volatile關鍵字有了明確的語義——在JDK1.5以前,volatile是個關鍵字,可是並無明確的規定其用途——被volatile修飾的寫變量不能和以前的讀寫代碼調整,讀變量不能和以後的讀寫代碼調整!所以,只要咱們簡單的把instance加上volatile關鍵字就能夠了。
 
public  class SingletonClass { 

   private  volatile  static SingletonClass instance =  null

   public  static SingletonClass getInstance() { 
     if (instance ==  null) { 
       synchronized (SingletonClass. class) { 
         if(instance ==  null) { 
          instance =  new SingletonClass(); 
        } 
      } 
    } 
     return instance; 
  } 

   private SingletonClass() { 

  } 
    
}
 
然而,這只是JDK1.5以後的Java的解決方案,那以前版本呢?其實,還有另外的一種解決方案,並不會受到Java版本的影響:
 
public  class SingletonClass { 
    
   private  static  class SingletonClassInstance { 
     private  static  final SingletonClass instance =  new SingletonClass(); 
  } 

   public  static SingletonClass getInstance() { 
     return SingletonClassInstance.instance; 
  } 

   private SingletonClass() { 

  } 
    
}
 
在這一版本的單例模式實現代碼中,咱們使用了Java的靜態內部類。這一技術是被JVM明確說明了的,所以不存在任何二義性。在這段代碼中,由於SingletonClass沒有static的屬性,所以並不會被初始化。直到調用getInstance()的時候,會首先加載SingletonClassInstance類,這個類有一個static的SingletonClass實例,所以須要調用SingletonClass的構造方法,而後getInstance()將把這個內部類的instance返回給使用者。因爲這個instance是static的,所以並不會構造屢次。
 
因爲SingletonClassInstance是私有靜態內部類,因此不會被其餘類知道,一樣,static語義也要求不會有多個實例存在。而且,JSL規範定義,類的構造必須是原子性的,非併發的,所以不須要加同步塊。一樣,因爲這個構造是併發的,因此getInstance()也並不須要加同步。
 
至此,咱們完整的瞭解了單例模式在Java語言中的時候,提出了兩種解決方案。我的偏向於第二種,而且Effiective Java也推薦的這種方式。
相關文章
相關標籤/搜索