Java設(shè)計模式之單例模式的究極版寫法

redis_logo

單例模式可能是后端學(xué)習(xí)者接觸到的第一種設(shè)計模式,可是單例模式真的有那么簡單嗎?在并發(fā)模式下會出現(xiàn)什么樣的問題?在學(xué)習(xí)了前面的并發(fā)知識后,我們來看看究極版的單例模式應(yīng)該怎么寫。


一、單例模式第一版

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

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

  • instance是Singleton類的靜態(tài)成員,也是我們的單例對象。它的初始值可以寫成Null,也可以寫成new Singleton()。至于其中的區(qū)別后來會做解釋。

  • getInstance是獲取單例對象的方法。

這兩個名字很形象:餓漢主動找食物吃,懶漢躺在地上等著人喂。
1、餓漢式:在程序啟動或單件模式類被加載的時候,單件模式實例就已經(jīng)被創(chuàng)建。
2、懶漢式:當(dāng)程序第一次訪問單件模式實例時才進(jìn)行創(chuàng)建。

懶漢模式加載快執(zhí)行慢,但是有線程安全問題,容易引起不同步問題,所以應(yīng)該創(chuàng)建同步"鎖"。

二、單例模式第二版

懶漢模式的線程安全問題主要在if (instance == null)這句判斷是否為空上。在多線程的環(huán)境下,可能有多個線程同時通過這個判斷。這樣一來,就有可能同時創(chuàng)建多個實例。讓我們來對代碼做一下修改:

public class Singleton {
    private Singleton() {}  //私有構(gòu)造函數(shù)
   private static Singleton instance = null;  //單例對象
   //靜態(tài)工廠方法
   public static Singleton getInstance() {
        if (instance == null) {      //雙重檢測機(jī)制
            synchronized (Singleton.class){  //同步鎖
                if (instance == null) {     //雙重檢測機(jī)制
                    instance = new Singleton();
                }
            }
         }
        return instance;
    }
}
  • 為了防止new Singleton被執(zhí)行多次,因此在new操作之前加上Synchronized 同步鎖,鎖住整個類(注意,這里不能使用對象鎖)。

  • 進(jìn)入Synchronized 臨界區(qū)以后,還要再做一次判空。因為當(dāng)兩個線程同時訪問的時候,線程A構(gòu)建完對象,線程B也已經(jīng)通過了最初的判空驗證,不做第二次判空的話,線程B還是會再次構(gòu)建instance對象。

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

三、單例模式第三版

假設(shè)這樣的場景,當(dāng)兩個線程一先一后訪問getInstance方法的時候,當(dāng)A線程正在構(gòu)建對象,B線程剛剛進(jìn)入方法。

這種情況表面看似沒什么問題,要么Instance還沒被線程A構(gòu)建,線程B執(zhí)行 if(instance == null)的時候得到true;要么Instance已經(jīng)被線程A構(gòu)建完成,線程B執(zhí)行 if(instance == null)的時候得到false。

我們之前在JAVA并發(fā)編程(一):理解volatile關(guān)鍵字學(xué)習(xí)過指令重排的知識,instance = new Singleton()這個操作不是一個原子操作,它在執(zhí)行的時候要經(jīng)歷以下三個步驟:

memory =allocate();    //1:分配對象的內(nèi)存空間 
ctorInstance(memory);  //2:初始化對象 
instance =memory;     //3:設(shè)置instance指向剛分配的內(nèi)存地址 

所以這里有可能出現(xiàn)如下情況:

當(dāng)線程A執(zhí)行完1,3,時,instance對象還未完成初始化,但已經(jīng)不再指向null。此時如果線程B搶占到CPU資源,執(zhí)行 if(instance == null)的結(jié)果會是false,從而返回一個沒有初始化完成的instance對象。

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

public class Singleton {
    private Singleton() {}  //私有構(gòu)造函數(shù)
    private volatile static Singleton instance = null;  //單例對象
    //靜態(tài)工廠方法
    public static Singleton getInstance() {
          if (instance == null) {      //雙重檢測機(jī)制
         synchronized (Singleton.class){  //同步鎖
           if (instance == null) {     //雙重檢測機(jī)制
             instance = new Singleton();
                }
             }
          }
          return instance;
      }
}

三、其他方式實現(xiàn)單例模式

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

①靜態(tài)內(nèi)部類實現(xiàn)單例模式

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

需要注意的是:

  • 從外部無法訪問靜態(tài)內(nèi)部類LazyHolder,只有當(dāng)調(diào)用Singleton.getInstance方法的時候,才能得到單例對象INSTANCE。

  • INSTANCE對象初始化的時機(jī)并不是在單例類Singleton被加載的時候,而是在調(diào)用getInstance方法,使得靜態(tài)內(nèi)部類LazyHolder被加載的時候。因此這種實現(xiàn)方式是利用classloader的加載機(jī)制來實現(xiàn)懶加載,并保證構(gòu)建單例的線程安全。

  • 靜態(tài)內(nèi)部類與餓漢&懶漢模式存在共同的問題:無法防止利用反射來重復(fù)構(gòu)建對象。

②枚舉實現(xiàn)單例模式

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

public class SingletonExample {

    // 私有構(gòu)造函數(shù)
    private SingletonExample() {

    }

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

    private enum Singleton {
        INSTANCE;

        private SingletonExample singleton;

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

        public SingletonExample getInstance() {
            return singleton;
        }
    }
}
  • 使用枚舉實現(xiàn)的單例模式不僅能夠防止反射構(gòu)造對象,而且可以保證線程安全。不過這種方式也有一個缺點,那就是不能實現(xiàn)懶加載,它的單例模式是在枚舉類被加載的時候進(jìn)行初始化的。

參考文章

漫畫:什么是單例模式?


本文作者: catalinaLi
本文鏈接: http://catalinali.top/2018/singletonPattern/
版權(quán)聲明: 原創(chuàng)文章,有問題請評論中留言。非商業(yè)轉(zhuǎn)載請注明作者及出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容