對於單例模式面試官會怎樣提問呢?你又該如何回答呢?

Ccww發表於2020-06-06

前言

在面試的時候面試官會怎麼在單例模式中提問呢?你又該如何回答呢?可能你在面試的時候你會碰到這些問題:

  • 為什麼說餓漢式單例天生就是執行緒安全的?
  • 傳統的懶漢式單例為什麼是非執行緒安全的?
  • 怎麼修改傳統的懶漢式單例,使其執行緒變得安全?
  • 執行緒安全的單例的實現還有哪些,怎麼實現?
  • 雙重檢查模式、Volatile關鍵字 在單例模式中的應用
  • ThreadLocal 在單例模式中的應用
  • 列舉式單例

那我們該怎麼回答呢?那答案來了,看完接下來的內容就可以跟面試官嘮嘮單例模式了


單例模式簡介

單例模式是一種常用的軟體設計模式,其屬於建立型模式,其含義即是一個類只有一個例項,併為整個系統提供一個全域性訪問點 (向整個系統提供這個實)。

結構:

單例模式三要素:

  • 私有的構造方法;
  • 私有靜態例項引用;
  • 返回靜態例項的靜態公有方法。

單例模式的優點

  • 在記憶體中只有一個物件,節省記憶體空間;
  • 避免頻繁的建立銷燬物件,可以提高效能;
  • 避免對共享資源的多重佔用,簡化訪問;
  • 為整個系統提供一個全域性訪問點。

單例模式的注意事項

  在使用單例模式時,我們必須使用單例類提供的公有工廠方法得到單例物件,而不應該使用反射來建立,使用反射將會破壞單例模式 ,將會例項化一個新物件。

單執行緒實現方式

在單執行緒環境下,單例模式根據例項化物件時機的不同分為,

  • 餓漢式單例(立即載入)餓漢式單例在單例類被載入時候,就例項化一個物件並將引用所指向的這個例項;
  • 懶漢式單例(延遲載入),只有在需要使用的時候才會例項化一個物件將引用所指向的這個例項。

從速度和反應時間角度來講,餓漢式(又稱立即載入)要好一些;從資源利用效率上說,懶漢式(又稱延遲載入)要好一些。


餓漢式單例

// 餓漢式單例
public class HungrySingleton{

    // 私有靜態例項引用,建立私有靜態例項,並將引用所指向的例項
    private static HungrySingleton singleton = new HungrySingleton();
    // 私有的構造方法
    private HungrySingleton(){}
    //返回靜態例項的靜態公有方法,靜態工廠方法
    public static HungrySingleton getSingleton(){
        return singleton;
    }
}

餓漢式單例,在類被載入時,就會例項化一個物件並將引用所指向的這個例項;更重要的是,由於這個類在整個生命週期中只會被載入一次,只會被建立一次,因此惡漢式單例執行緒安全的。


那餓漢式單例為什麼是天生就執行緒安全呢?

因為類載入的方式是按需載入,且只載入一次。由於一個類在整個生命週期中只會被載入一次,線上程訪問單例物件之前就已經建立好了,且僅此一個例項。即執行緒每次都只能也必定只可以拿到這個唯一的物件。


懶漢式單例

// 懶漢式單例
public class LazySingleton {
    // 私有靜態例項引用
    private static LazySingleton singleton;
    // 私有的構造方法
    private LazySingleton(){}
    // 返回靜態例項的靜態公有方法,靜態工廠方法
    public static LazySingleton getSingleton(){
        //當需要建立類的時候建立單例類,並將引用所指向的例項
        if (singleton == null) {
            singleton = new LazySingleton();
        }
        return singleton;
    }
}

懶漢式單例是延遲載入,只有在需要使用的時候才會例項化一個物件,並將引用所指向的這個物件。

由於是需要時建立,在多執行緒環境是不安全的,可能會併發建立例項,出現多例項的情況,單例模式的初衷是相背離的。那我們需要怎麼避免呢?可以看接下來的多執行緒中單例模式的實現形式。


那為什麼傳統的懶漢式單例為什麼是非執行緒安全的?

非執行緒安全主要原因是,會有多個執行緒同時進入建立例項(if (singleton == null) {}程式碼塊)的情況發生。當這種這種情形發生後,該單例類就會建立出多個例項,違背單例模式的初衷。因此,傳統的懶漢式單例是非執行緒安全的。


多執行緒實現方式

  在單執行緒環境下,無論是餓漢式單例還是懶漢式單例,它們都能夠正常工作。但是,在多執行緒環境下就有可能發生變異:

  • 餓漢式單例天生就是執行緒安全的,可以直接用於多執行緒而不會出現問題
  • 懶漢式單例本身是非執行緒安全的,因此就會出現多個例項的情況,與單例模式的初衷是相背離的。

那我們應該怎麼在懶漢的基礎上改造呢?

  • synchronized方法
  • synchronized塊
  • 使用內部類實現延遲載入

synchronized方法

// 執行緒安全的懶漢式單例
public class SynchronizedSingleton {
    private static SynchronizedSingleton synchronizedSingleton;
    private SynchronizedSingleton(){}
    // 使用 synchronized 修飾,臨界資源的同步互斥訪問
    public static synchronized SynchronizedSingleton getSingleton(){
        if (synchronizedSingleton == null) {
            synchronizedSingleton = new SynchronizedSingleton();
        }
        return synchronizedSingleton;
    }
}

  使用 synchronized 修飾 getSingleton()方法,將getSingleton()方法進行加鎖,實現對臨界資源的同步互斥訪問,以此來保證單例。

雖然可現實執行緒安全,但由於同步的作用域偏大、鎖的粒度有點粗,會導致執行效率會很低。


synchronized塊

// 執行緒安全的懶漢式單例
public class BlockSingleton {
    private static BlockSingleton singleton;
    private BlockSingleton(){}
    public static BlockSingleton getSingleton2(){
        synchronized(BlockSingleton.class){  // 使用 synchronized 塊,臨界資源的同步互斥訪問
            if (singleton == null) { 
                singleton = new BlockSingleton();
            }
        }
        return singleton;
    }
}

 其實synchronized塊跟synchronized方法類似,效率都偏低。


使用內部類實現延遲載入

// 執行緒安全的懶漢式單例
public class InsideSingleton {
    // 私有內部類,按需載入,用時載入,也就是延遲載入
    private static class Holder {
        private static InsideSingleton insideSingleton = new InsideSingleton();
    }
    private InsideSingleton() {
    }
    public static InsideSingleton getSingleton() {
        return Holder.insideSingleton;
    }
}
  • 如上述程式碼所示,我們可以使用內部類實現執行緒安全的懶漢式單例,這種方式也是一種效率比較高的做法。其跟餓漢式單例原理是相同的, 但可能還存在反射攻擊或者反序列化攻擊 。

雙重檢查(Double-Check idiom)現實

雙重檢查(Double-Check idiom)-volatile

使用雙重檢測同步延遲載入去建立單例,不但保證了單例,而且提高了程式執行效率。

// 執行緒安全的懶漢式單例
public class DoubleCheckSingleton {
    //使用volatile關鍵字防止重排序,因為 new Instance()是一個非原子操作,可能建立一個不完整的例項
    private static volatile DoubleCheckSingleton singleton;
    private DoubleCheckSingleton() {
    }

    public static DoubleCheckSingleton getSingleton() {
        // Double-Check idiom
        if (singleton == null) {
            synchronized (DoubleCheckSingleton.class) {       
                // 只需在第一次建立例項時才同步
                if (singleton == null) {      
                    singleton = new DoubleCheckSingleton();      
                }
            }
        }
        return singleton;
    }

}

為了在保證單例的前提下提高執行效率,我們需要對singleton例項進行第二次檢查,為的式避開過多的同步(因為同步只需在第一次建立例項時才同步,一旦建立成功,以後獲取例項時就不需要同步獲取鎖了)。

但需要注意的必須使用volatile關鍵字修飾單例引用,為什麼呢?

 如果沒有使用volatile關鍵字是可能會導致指令重排序情況出現,在Singleton 建構函式體執行之前,變數 singleton可能提前成為非 null 的,即賦值語句在物件例項化之前呼叫,此時別的執行緒將得到的是一個不完整(未初始化)的物件,會導致系統崩潰。

此可能為程式執行步驟:

  1. 執行緒 1 進入 getSingleton() 方法,由於 singleton 為 null,執行緒 1 進入 synchronized 塊 ;
  2. 同樣由於 singleton為 null,執行緒 1 直接前進到 singleton = new DoubleCheckSingleton()處,在new物件的時候出現重排序,導致在建構函式執行之前,使例項成為非 null,並且該例項並未初始化的(原因在NOTE);
  3. 此時,執行緒 2 檢查例項是否為 null。由於例項不為 null,執行緒 2 得到一個不完整(未初始化)的 Singleton 物件
  4. 執行緒 1 通過執行 Singleton物件的建構函式來完成對該物件的初始化。

  這種安全隱患正是由於指令重排序的問題所導致的。而volatile 關鍵字正好可以完美解決了這個問題。使用volatile關鍵字修飾單例引用就可以避免上述災難。

NOTE

new 操作會進行三步走,預想中的執行步驟:

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

但實際上,這個過程可能發生無序寫入(指令重排序),可能會導致所下執行步驟

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

雙重檢查(Double-Check idiom)-ThreadLocal

  藉助於 ThreadLocal,我們可以實現雙重檢查模式的變體。我們將臨界資源執行緒區域性化,具體到本例就是將雙重檢測的第一層檢測條件 if (instance == null) 轉換為 執行緒區域性範圍內的操作 。

// 執行緒安全的懶漢式單例
public class ThreadLocalSingleton 
    // ThreadLocal 執行緒區域性變數
    private static ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>();
    private static ThreadLocalSingleton singleton = null;
    private ThreadLocalSingleton(){}
    public static ThreadLocalSingleton getSingleton(){
        if (threadLocal.get() == null) {        // 第一次檢查:該執行緒是否第一次訪問
            createSingleton();
        }
        return singleton;
    }

    public static void createSingleton(){
        synchronized (ThreadLocalSingleton.class) {
            if (singleton == null) {          // 第二次檢查:該單例是否被建立
                singleton = new ThreadLocalSingleton();   // 只執行一次
            }
        }
        threadLocal.set(singleton);      // 將單例放入當前執行緒的區域性變數中 
    }
}

藉助於 ThreadLocal,我們也可以實現執行緒安全的懶漢式單例。但與直接雙重檢查模式使用,使用ThreadLocal的實現在效率上還不如雙重檢查鎖定。


列舉實現方式

它不僅能避免多執行緒同步問題,而且還能防止反序列化重新建立新的物件,

直接通過Singleton.INSTANCE.whateverMethod()的方式呼叫即可。方便、簡潔又安全。

public enum EnumSingleton {
    instance;
    public void whateverMethod(){
        //dosomething
    }
}

測試單例執行緒安全性

 使用多個執行緒,並使用hashCode值計算每個例項的值,值相同為同一例項,否則為不同例項。

public class Test {
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new TestThread();

        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();

        }
    }
}
class TestThread extends Thread {
    @Override
    public void run() {
        // 對於不同單例模式的實現,只需更改相應的單例類名及其公有靜態工廠方法名即可
        int hash = Singleton5.getSingleton5().hashCode();  
        System.out.println(hash);
    }
}

小結

單例模式是 Java 中最簡單,也是最基礎,最常用的設計模式之一。在執行期間,保證某個類只建立一個例項,保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點 ,介紹單例模式的各種寫法:

  • 餓漢式單例(執行緒安全)
  • 懶漢式單例

    • 傳統懶漢式單例(執行緒安全);
    • 使用synchronized方法實(執行緒安全);
    • 使用synchronized塊實現懶漢式單例(執行緒安全);
    • 使用靜態內部類實現懶漢式單例(執行緒安全)。
  • 使用雙重檢查模式

    • 使用volatile關鍵字(執行緒安全);
    • 使用ThreadLocal實現懶漢式單例(執行緒安全)。
  • 列舉式單例
各位看官還可以嗎?喜歡的話,動動手指點個?,點個關注唄!!謝謝支援!
歡迎掃碼關注,原創技術文章第一時間推出

相關文章