1. 程式人生 > >Java設計模式之單例模式的究極版寫法

Java設計模式之單例模式的究極版寫法

redis_logo

單例模式可能是後端學習者接觸到的第一種設計模式,可是單例模式真的有那麼簡單嗎?在併發模式下會出現什麼樣的問題?在學習了前面的併發知識後,我們來看看究極版的單例模式應該怎麼寫。

一、單例模式第一版

我們最初接觸到的單例模式一般就是懶漢模式與餓漢模式。我們先來看看怎麼寫:

//懶漢模式
public class Singleton {
    private Singleton() {}  //私有建構函式
    private static Singleton instance = null;  //單例物件
    //靜態工廠方法
    public static Singleton getInstance
() { if (instance == null) { instance = new Singleton(); } return instance; } }
//餓漢模式
public class Singleton {
    private Singleton() {}  //私有建構函式
    private static Singleton instance = new Singleton();  //單例物件
    //靜態工廠方法
    public static Singleton getInstance(
) { return instance; } }
  • 要想讓一個類只能構建一個物件,自然不能讓它隨便去做new操作,因此Signleton的構造方法是私有的。

  • instance是Singleton類的靜態成員,也是我們的單例物件。它的初始值可以寫成Null,也可以寫成new Singleton()。至於其中的區別後來會做解釋。

  • getInstance是獲取單例物件的方法。

這兩個名字很形象:餓漢主動找食物吃,懶漢躺在地上等著人喂。
1、餓漢式:在程式啟動或單件模式類被載入的時候,單件模式例項就已經被建立。
2、懶漢式:當程式第一次訪問單件模式例項時才進行建立。

懶漢模式載入快執行慢,但是有執行緒安全問題,容易引起不同步問題,所以應該建立同步"鎖"。

二、單例模式第二版

懶漢模式的執行緒安全問題主要在if (instance == null)這句判斷是否為空上。在多執行緒的環境下,可能有多個執行緒同時通過這個判斷。這樣一來,就有可能同時建立多個例項。讓我們來對程式碼做一下修改:

public class Singleton {
    private Singleton() {}  //私有建構函式
   private static Singleton instance = null;  //單例物件
   //靜態工廠方法
   public static Singleton getInstance() {
        if (instance == null) {      //雙重檢測機制
            synchronized (Singleton.class){  //同步鎖
                if (instance == null) {     //雙重檢測機制
                    instance = new Singleton();
                }
            }
         }
        return instance;
    }
}
  • 為了防止new Singleton被執行多次,因此在new操作之前加上Synchronized 同步鎖,鎖住整個類(注意,這裡不能使用物件鎖)。

  • 進入Synchronized 臨界區以後,還要再做一次判空。因為當兩個執行緒同時訪問的時候,執行緒A構建完物件,執行緒B也已經通過了最初的判空驗證,不做第二次判空的話,執行緒B還是會再次構建instance物件。

然而,這種方法也有一定的缺席。

三、單例模式第三版

假設這樣的場景,當兩個執行緒一先一後訪問getInstance方法的時候,當A執行緒正在構建物件,B執行緒剛剛進入方法。

這種情況表面看似沒什麼問題,要麼Instance還沒被執行緒A構建,執行緒B執行 if(instance == null)的時候得到true;要麼Instance已經被執行緒A構建完成,執行緒B執行 if(instance == null)的時候得到false。

我們之前在JAVA併發程式設計(一):理解volatile關鍵字學習過指令重排的知識,instance = new Singleton()這個操作不是一個原子操作,它在執行的時候要經歷以下三個步驟:

memory =allocate();    //1:分配物件的記憶體空間 
ctorInstance(memory);  //2:初始化物件 
instance =memory;     //3:設定instance指向剛分配的記憶體地址 

所以這裡有可能出現如下情況:

當執行緒A執行完1,3,時,instance物件還未完成初始化,但已經不再指向null。此時如果執行緒B搶佔到CPU資源,執行 if(instance == null)的結果會是false,從而返回一個沒有初始化完成的instance物件。

如何避免這一情況呢?我們需要在instance物件前面增加一個修飾符volatile。

public class Singleton {
    private Singleton() {}  //私有建構函式
    private volatile static Singleton instance = null;  //單例物件
    //靜態工廠方法
    public static Singleton getInstance() {
          if (instance == null) {      //雙重檢測機制
         synchronized (Singleton.class){  //同步鎖
           if (instance == null) {     //雙重檢測機制
             instance = new Singleton();
                }
             }
          }
          return instance;
      }
}

三、其他方式實現單例模式

實現單例模式的手段還有很多,我們再來看一些別的實現方式。

①靜態內部類實現單例模式

public class Singleton {
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

需要注意的是:

  • 從外部無法訪問靜態內部類LazyHolder,只有當呼叫Singleton.getInstance方法的時候,才能得到單例物件INSTANCE。

  • INSTANCE物件初始化的時機並不是在單例類Singleton被載入的時候,而是在呼叫getInstance方法,使得靜態內部類LazyHolder被載入的時候。因此這種實現方式是利用classloader的載入機制來實現懶載入,並保證構建單例的執行緒安全。

  • 靜態內部類與餓漢&懶漢模式存在共同的問題:無法防止利用反射來重複構建物件。

②列舉實現單例模式

可以防止反射的無懈可擊的單例模式程式碼:

public class SingletonExample {

    // 私有建構函式
    private SingletonExample() {

    }

    public static SingletonExample getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;

        private SingletonExample singleton;

        // JVM保證這個方法絕對只調用一次
        Singleton() {
            singleton = new SingletonExample();
        }

        public SingletonExample getInstance() {
            return singleton;
        }
    }
}
  • 使用列舉實現的單例模式不僅能夠防止反射構造物件,而且可以保證執行緒安全。不過這種方式也有一個缺點,那就是不能實現懶載入,它的單例模式是在列舉類被載入的時候進行初始化的。

參考文章