天天看點

單例模式隻會懶漢餓漢?讀完本篇讓你面試瘋狂加分

前言

說到設計模式,面試排在第一位的十有八九是單例模式,這一定是大部分人從入門到面試工作都避不開的基礎知識。

但單例模式不僅有懶漢模式和餓漢模式兩種寫法,往往我們掌握的都是最基礎的寫法,如果你有閱讀過類似spring這樣的知名架構源碼,一定會發現他們的單例模式寫法和你所掌握的完全不同。

本篇就給大家帶來單例模式從基礎->最優->額外推薦的寫法,幫助你面試瘋狂加分。

懶漢餓漢

1、餓漢模式

餓漢模式簡單了解就是提前建立好了對象

優點:寫法簡單,沒有線程同步的問題

缺點:因為要提前建立好對象,不管使用與否都一直占着記憶體

推薦:對象較小且簡單則使用餓漢模式

public final class Singleton { 
    // 建立好執行個體 
    private static Singleton instance = new Singleton();
    // 構造函數 
    private Singleton() {} 
    // 擷取執行個體 
    public static Singleton getInstance() {
        return instance; 
    } 
}
           

2、懶漢模式

懶漢模式簡單了解就是在需要時才建立對象

優點:懶加載方式性能更高

缺點:要考慮多線程的同步問題

推薦:隻要不符合上面餓漢的推薦使用條件則都使用懶漢模式

public final class Singleton { 
    private static Singleton instance = null;
    // 構造函數 
    private Singleton() {} 
    // 擷取執行個體
    public static Singleton getInstance() {
        // 為null時才執行個體化對象
        if (null == instance) {
            instance = new Singleton();
        } 
        return instance;
    } 
}
           

同步鎖

上面介紹了懶漢的缺點是多線程同步的問題,那麼馬上就能想到使用同步鎖來解決這個問題。

這裡使用synchronized關鍵字且通過代碼塊來降低鎖粒度,最大程度保證了性能開銷,其實從java8以後,synchronized的性能已經有了較大提升。

public final class Singleton { 
    private static Singleton instance = null;
    // 構造函數 
    private Singleton() {}
    // 擷取執行個體
    public static Singleton getInstance() {
        // 擷取對象時加上同步鎖
        if (null == instance) {
            synchronized (Singleton.class) { 
                instance = new Singleton();
            } 
        } 
        return instance;
    } 
}
           

雙重檢查鎖

上面雖然使用了同步鎖代碼塊,勉強解決了線程同步的問題且性能開銷做了最大程度的優化,可實際上在多線程環境下仍然存線上程安全問題。

當依然有多個線程進入到if判斷裡面時,這個線程安全問題還是存在,雖然這種情況并非一定出現,可極端情況下出現的幾率非常大。

這個時候就需要使用面試中關于設計模式很喜歡問到的DCL即雙重檢查鎖模式,聽起來很高大上,其實就是多加了一層判斷。

說白了,就是在進入同步鎖之前和之後分别進行了檢查,極大降低了線程安全問題。

public final class Singleton { 
    private static Singleton instance = null;
    // 構造函數 
    private Singleton() {} 
    // 擷取執行個體
    public static Singleton getInstance() {
        // 第一次判斷,當instance為null時則執行個體化對象
        if(null == instance) {
            synchronized (Singleton.class) {
                // 第二次判斷,放在同步鎖中,當instance為null時則執行個體化對象 
                if(null == instance) {
                    instance = new Singleton();
                } 
            } 
        } 
        return instance;
    } 
}
           

最優雙重檢查鎖

雙重檢查鎖方式是單例懶漢模式在多線程下處理安全問題的最佳方案之一,但上面依然不是最優寫法。

這裡就要引出一個「指令重排」的概念,這個概念是java記憶體模型中的,我這裡用最簡潔的方式幫你了解。

Java中new一個對象在記憶體中執行指令的正常順序是:配置設定 -> 建立 -> 引用,而多線程環境下,JVM出于對語句的優化,有可能重排順序:配置設定 -> 引用 -> 建立。

如果出現這種情況,那麼上面的雙重檢查鎖方式依然無法解決線程安全問題。

解決方式很簡單,加個volatile關鍵字即可。

volatile關鍵字作用:保證可見性和有序性。

public final class Singleton { 
    // 加上volatile關鍵字
    private volatile static Singleton instance = null;
    // 構造函數 
    private Singleton() {} 
    // 擷取執行個體
    public static Singleton getInstance() {
        // 第一次判斷,當instance為null時則執行個體化對象
        if(null == instance) {
            synchronized (Singleton.class) {
                // 第二次判斷,放在同步鎖中,當instance為null時則執行個體化對象 
                if(null == instance) {
                    instance = new Singleton();
                } 
            } 
        } 
        return instance;
    } 
}
           

枚舉模式

《Effective Java》是Java業界非常受歡迎的一本書,對于想要在Java領域深耕的程式員來講,這本書沒有不看的理由,相信很多Java程式員不管看過還是沒看過,都有聽過這本書。

而這本書的作者,所推薦的一種單例設計模式寫法,就是枚舉方式。

原理十分簡單,在Java中枚舉類的域在編譯後會被聲明為static屬性,而JVM會保證static修飾的成員變量隻被執行個體化一次。

public class Singleton {

   // 構造函數
   private Singleton() {
   }

   // 從枚舉中擷取執行個體
   public static Singleton getInstance() {
      return SingletonEnum.SINGLETON.getInstance();
   }

   // 定義枚舉
   private enum SingletonEnum {
      SINGLETON;

      private Singleton instance;

      // JVM保證這個方法隻調用一次
      SingletonEnum() {
         instance = new Singleton();
      }

      public Singleton getInstance() {
         return instance;
      }
   }
}
           

總結

最後這裡稍微提一下,以免部分人對于設計模式感到些許負擔。

單例模式其實很簡單,餓漢模式和懶漢模式在許多開源架構中應用都比較廣泛,甚至餓漢模式用的更多,比如Java的Runtime類中就這麼幹的,簡單粗暴,有興趣的可以自己看下源碼。

難道這些架構的作者就意識不到本篇中講述的問題嗎,并非如此,用哪種方式編寫單例模式往往視情況而定,一些理論上會發生的問題往往實際中可以忽略不計,此時更傾向于使用最簡單直接的寫法。

真正難的其實還是面試,不少關于單例模式的問題中喜歡問到它的幾種寫法、存在的問題以及最佳方案,說白了還是面試造核彈,進廠擰螺絲,目的是想知道你對設計模式的了解程度,進而評判你研究該學科的态度及造詣。

是以,大家看完本篇可以手動嘗試着寫一寫,了解一些也就夠了,沒必要過分深究,因為Java領域需要花費精力的地方确實太多了。

心得

最後說下我的一點心得,所謂設計模式固然能給Java代碼本身帶來更多優雅,但是寫了很多年Java代碼,我大體還是覺得Java本身的裝飾實在太多,優雅換來的往往是代碼本身的負擔。

我參與過的研發團隊中,幾乎都能見到許多工程師編寫的比較優雅的代碼,一些設計模式也寫的很好,可帶來的問題也很明顯,就是可讀性越來越差,要求每個團員都對Java有較高的造詣,甚至在某些時候給人力資源帶來壓力,這從實際角度考慮是不妥的。

我更多的建議是,應對面試或學習好好領悟設計模式,百利而無一害,但實際工作中盡量少用複雜的設計模式,以簡潔直接的代碼為主,有利于整個團隊後期維護,甚至加快人員變更後新成員對項目的适應度,因為工作說白了還是以績效為主,怎麼簡單高效怎麼來就行,你自己的個人項目你想怎麼玩随便你。

本人原創文章純手打,覺得有一滴滴幫助的話就請點個推薦吧~

本人長期分享工作中的感悟、經驗及實用案例,喜歡的話也可以關注一下哦~

喜歡就點一下推薦吧~~