單例模式的DCL方式,您不可不知道的知識點

 單例模式的DCL是一種比較好的單例實現方式,面試中被問及的頻率非常高,考察的方式也多種多樣。這裡簡單整理了一下,這裡面的每一個點最好都能夠做到爛熟於心:

複製程式碼
 1 public class Test {
 2     private volatile static Test instance;
 3 
 4     private Test() {
 5 
 6     }
 7 
 8     public static Test getInstance() {
 9         if (instance == null) {
10             synchronized (Test.class) {
11                 if (instance == null) {
12                     instance = new Test();
13                 }
14             }
15         }
16         return instance;
17     }
18 }
複製程式碼

 這裡有5個要點需要注意:

    (1)第一個注意點:使用私有的構造函數,確保正常情況下該類不能被外部初始化(非正常情況比如通過反射初始化,一般使用反射之後單例模式也就失去效果了)。

    (2)第二個注意點:getInstance方法中第一個判空條件,邏輯上是可以去除的,去除之後並不影響單例的正確性,但是去除之後效率低。因為去掉之後,不管instance是否已經初始化,都會進行synchronized操作,而synchronized是一個重操作消耗性能。加上之後,如果已經初始化直接返回結果,不會進行synchronized操作。

    (3)第三個注意點:加上synchronized是為了防止多個執行緒同時調用getInstance方法時,各初始化instance一遍的並發問題。

    (4)第四個注意點:getInstance方法中的第二個判空條件是不可以去除,如果去除了,並且剛好有兩個執行緒a和b都通過了第一個判空條件。此時假設a先獲得鎖,進入synchronized的程式碼塊,初始化instance,a釋放鎖。接著b獲得鎖,進入synchronized的程式碼塊,也直接初始化instance,instance被初始化多遍不符合單例模式的要求~。加上第二個判空條件之後,b獲得鎖進入synchronized的程式碼塊,此時instance不為空,不執行初始化操作。

    (5)第五個注意點:instance的聲明有一個voliate關鍵字,如果不用該關鍵字,有可能會出現異常。因為instance = new Test();並不是一個原子操作,會被編譯成三條指令,如下所示。
          1)給Test的實例分配記憶體

          2)初始化Test的構造器

          3)將instance對象指向分配的記憶體空間(注意,此時instance就不為空)

        然後咧,java會指令重排序,JVM根據處理器的特性,充分利用多級快取,多核等進行適當的指令重排序,使程式在保證業務運行的同時,充分利用CPU的執行特點,最大的發揮機器的性能!簡單來說就是jvm執行上面三條指令的時候,不一定是1-2-3這樣執行,有可能是1-3-2這樣執行。如果jvm是按照1-3-2來執行的話,當1-3執行完2還沒執行的時候,如果另外一個執行緒調用getInstance(),因為3執行了此時instance不為空,直接返回instance。問題是2還沒執行,此時instance相當於什麼都沒有,肯定是有問題的。然後咧,voliate有一個特性就是禁止指令重排序,上面的三條指令是按照1-2-3執行的,這樣就沒有問題了。