單例模式實現延遲加載在多線程下的思考(雙檢鎖和內部類)

本文參考了《java併發編程藝術》一書中的部分內容,增加了部分自己的理解。

1、單例模式最簡單的實現:
  1. class  Single{
  2.        public static Single single = new Single();
  3.        private Single(){}
  4.        public static getInstance(){
  5.               return single ;
  6.        }
  7. }
此方法的缺陷是不能實現延遲加載。

2、通過synchronized實現延遲加載
  1. class  LazySingle{
  2.        private static LazySingle single = null
  3.        private LazySingle(){}
  4.        public static synchronized getInstance(){
  5.               if(single  == null ){
  6.                        single  = new LazySingle();
  7.               }
  8.               return single ;
  9.        }
  10. }
此方法存在性能問題,多線程調用getInstance()方法,都需要檢查鎖是否存在;


3、通過雙檢鎖實現

  1. class  LazySingle{
  2.        private static LazySingle single = null
  3.        private LazySingle(){}
  4.        public static  getInstance(){
  5.               if(single  == null ){
  6.                       synchronized(LazySingle.class){
  7.                             if(single  == null){
  8.                                  single  = new LazySingle(); 
  9.                             }
  10.                        }
  11.               }
  12.               return single ;
  13.        }
  14. }

此方法看起來貌似很完美,當single沒有實例化走同步方法,當single已經實例化則不需要走同步方法,有效的解決了性能問題。但是此方法是一個錯誤的優化。
當線程執行到第5行時,代碼讀取到single不爲空時,single引用的對象可能還沒有完成初始化。
我們對第8行代碼進行分析(single = new LazySingle()), 這行代碼在編譯器中會被翻譯成如下僞代碼:

  1. memory = allocate();   //分配對象的內存空間
  2. ctorInstance(memory); //初始化對象
  3. instance = memory;     //設置instance指向剛分配的內存地址
上面第2行和3行代碼可能會被重排序(爲什麼會重排序,大家可以參考《java併發編程藝術》一書),重排序的結果爲
  1. memory = allocate();   //分配對象的內存空間
  2. instance = memory;     //設置instance指向剛分配的內存地址
  3. ctorInstance(memory); //初始化對象
 此時,如果2行執行結束,剛好有個線程訪問到雙檢索代碼的第5行時,single並不爲空,但是初始化還未完成,將會返回一個空的引用。 
解決這個問題的方法有兩種。

4、通過volatile解決
  1. class  LazySingle{
  2.        private volatile static LazySingle single = null
  3.        private LazySingle(){}
  4.        public static  getInstance(){
  5.               if(single  == null ){
  6.                       synchronized(LazySingle.class){
  7.                             if(single  == null){
  8.                                  single  = new LazySingle(); 
  9.                             }
  10.                        }
  11.               }
  12.               return single ;
  13.        }
  14. }

5、內部類方式解決
JVM在執行類的初始化期間,會獲取一個鎖,這個鎖可以同步多個線程對同一個類的初始化。
初始化一個類包括執行這個類的靜態初始化和初始化在這個類中聲明的靜態字段。
因此當在單例類中定義 public static Single single = new Single(); 的變量時,不具備延遲加載的特性,JVM在load到這個類時就就行了變量的初始化。

根據java語言規範,在首次發生如下任意情況時,一個類或者接口類型T將被立即初始化:
  •     T是一個類,而且一個T類型的實例被創建;
  •     T是一個類,且T中聲明的一個靜態方法被調用;
  •     T中聲明的一個靜態字段被賦值;
  •     T中聲明的一個靜態字段被使用,而且這個字段不是一個常量字段;
  •     T是一個頂級類,而且一個斷言語句嵌套在T內部被執行;
    
public class StaticSingle {
 private StaticSingle(){
 
 }
 private static class StaticSingleHolder{
      private static StaticSingle instance = new StaticSingle();
 }
 public static StaticSingle getInstance(){
      return StaticSingleHolder.instance;
 }
}

使用內部類的方式實現單例,既可以實現延遲加載,也不必使用同步關鍵字,是一種比較完善的實現


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章