[Java] [Singleton] [DCL][happens-before]

Singleton

  • 只能有一個實例;必須本身建立本身的實例;必須給其餘全部對象提供這一實例

實現方法

餓漢式singleton

  • 預先加載法
  • class Single {
      private Single() {
        System.out.println("ok");
      }
      
      private static Single instance = new Single();
      
      public static Single getInstance() {
        return instance;
      }
    }
  • 優勢:
    • thread safe
    • 調用時速度快(在類加載時已經建立好一個static對象)
  • 缺點:
    • 資源利用率不高(可能系統不須要)
    • 在一些場景下沒法使用。好比在single實例的建立依賴參數或配置文件時。

懶漢式singleton

  • 延遲加載法
  • public class LazySingleton {
      private static LazySingleton instance;
      private LazySingleton() {}
      
      public static LazySingleton getInstance() {
        if (instance == null) {
          instance = new LazySingleton();
        }
        return instance;
      }
    }
  • 適用於單線程環境,not trhead-safe,getInstance()方法可能返回兩個不一樣實例。
  • 能夠改爲thread-safe版本,以下:
    public class LazySingleton {
        private static LazySingleton instance;    
        private LazySingleton() {}
        
        public static synchronized LazySingleton getInstance() {             
    		if (instance == null) {                       
    			instance = new LazySingleton();           
    		}
            return instance;                                      
        }
    }
  • 優勢:不執行getInstance對不會被實例化
  • 缺點:第一次加載時反應不快。每次調用getInstance的同步開銷大。(大量沒必要要的同步)

DCL singleton

  • Double Check Lock
  • 避免每次調用getInstance方法時都同步
  • public class LazySingleton {
      private static LazySingleton instance;
      private LazySingleton() {}
      
      public static LazySingleton getInstance() {
        if (instance == null) {
          synchronized(LazySingleton.class) {
            if (instance == null) {
              instance = new LazySingleton();
            }
          }
        }
        return instance;
      }
    }
  • 第一層判斷,避免沒必要要的同步。第二層判斷則是在線程安全的狀況下建立實例。
  • 優勢:資源利用率高,多線程下效率高。
  • 缺點:第一次加載時反應不快,因爲java內存模型一些緣由偶爾會失敗,在高併發下有必定的缺陷。
  • 上述代碼依然存在不安全性
    instance = new LazySingleton()這條語句實際上不是一個原子操做,它大概包括三件事:
    1. 給LazySingleton的實例分配內存;
    2. 初始化LazySingleton()的構造器;
    3. 將instance對象指向分配的內存空間(在這一步的時候instance變成非null)。

  可是因爲Java編譯器容許處理器亂序執行(指令重排序),上述二、3點的順序是沒法保證的。(意思是可能instance != null時有可能還未真正初始化構造器)。 
  解決方法是經過將instance定義爲volatile的。(volatile有兩個語義:1. 保證變量的可見性;2. 禁止對該變量的指令重排序)java

  • 參考<<Java併發編程>> P286 ~ P287。在JMM後續版本(>= Java5.0)中,能夠經過結合volatile的方式來啓動DCL,而且該方式對性能的影響很小。然而,DCL的這種使用方式已經被普遍地拋棄了。
  • (由於volatile屏蔽指令重排序的語義在JDK1.5中才被徹底修復,此前的JDK中即便將變量聲明爲volatile,也仍然不能徹底避免重排序所致使的問題,這主要是由於volatile變量先後的代碼仍然存在重排序問題。)
  • (Java5中多了一條happens-before的規則:對volatile字段的寫操做happens-before後續對同一個字段的讀操做)

static內部類singleton

  • class Single {
      private Single() {}
      
      private static class InstanceHolder {
        private static final Single instance = new Single();
      }
      
      public static Single getInstance() {
        return InstanceHolder.instance();
      }
    }
  • 優勢:線程安全,資源利用率高。
  • 缺點:第一次加載時反應不快。
  • 原理:類級內部類(static修飾的成員內部類)只有在第一次使用時纔會被加載

Summary

  • 考慮到效率、安全性等問題,通常經常使用餓漢式singleton or static內部類singleton。其中後者是經常使用的singleton實現方法。

Happens-before

  • 是否能夠經過幾個基本的happens-before規則從理論上分析Java多線程程序的正確性,而不須要設計到硬件和編譯器的知識呢?

Happens-before規則

  • 通俗來講,A happens-before B意味着操做A對內存施加的影響都能被B觀測到。
  • 關於happens-before:
    • happens-before relation on memory operations such reads and writes of shared varaiables.
    • In particular:
      • Each action in a thread happens-before every action in that thread that comes later in the program's order. (單線程規則
      • An unlock (synchronized block or method exit) of a monitor happens-before every subsequent lock (synchronized block or method entry) of that same monitor. And because the happens-before relation is transitive, all actions of a thread prior to unlocking happen-before all actions subsequent to any thread locking that monitor. (線程安全性主要依賴這條規則)
      • A write to a volatile field happens-before every subsequent read of that same field. Writes and reads of volatile fields have similar memory consistency effects as entering and exiting monitors, but do not entail mutual exclusion locking.
      • A call to start on a thread happens-before any action in the started thread.
      • All actions in a thread happen-before any other thread successfully returns from a join on that thread.
      • happens-before關係具備傳遞性。 hb(A, B) + hb(B, C) => hb(A, C)
  • 要知道,「A在時間上先於B」和「A happens-before B」二者並不等價。
  • 兩個操做之間必然存在某種時序關係(前後or同時),但兩個操做之間卻不必定存在happens-before關係。但兩個存在happens-before關係的操做不可能同時發生,這實際上也是同步的語義之一(獨佔訪問)。
  • 以及,上述一直提到的操做並不等同於語句。操做應該是單個虛擬機指令,單條語句可能由多個指令組成。

Happens-before & DCL

  • DCL(without volatile)的主要問題在於儘管獲得了LazySingleton的引用,但卻有可能訪問到其成員變量的不正確值。
  • 從新分析上述DCL例子:
    public class LazySingleton {  
        private int someField;  
          
        private static LazySingleton instance;  
          
        private LazySingleton() {  
            this.someField = new Random().nextInt(200)+1;         // (1)  
        }  
          
        public static LazySingleton getInstance() {  
            if (instance == null) {                               // (2)  
                synchronized(LazySingleton.class) {               // (3)  
                    if (instance == null) {                       // (4)  
                        instance = new LazySingleton();           // (5)  
                    }  
                }  
            }  
            return instance;                                      // (6)  
        }  
          
        public int getSomeField() {  
            return this.someField;                                // (7)  
        }  
    } 
  • DCL產生安全問題的主要緣由就在於:(1) & (7) 之間不存在happens-before關係。
  • 這個例子中LazySingleton是一個不變類,它只有get而沒有set方法。但上述例子讓咱們知道,即便一個對象是不變的,在不一樣的線程中也可能返回不一樣值。這是由於LazySingleton沒有被安全地發佈
相關文章
相關標籤/搜索