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

前言

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

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

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


單例模式簡介

單例模式是一種經常使用的軟件設計模式,其屬於建立型模式,其含義便是一個類只有一個實例,併爲整個系統提供一個全局訪問點 (向整個系統提供這個實)。設計模式

結構:
安全

單例模式三要素:多線程

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

單例模式的優勢

  • 在內存中只有一個對象,節省內存空間;
  • 避免頻繁的建立銷燬對象,能夠提升性能;
  • 避免對共享資源的多重佔用,簡化訪問;
  • 爲整個系統提供一個全局訪問點。

單例模式的注意事項

  在使用單例模式時,咱們必須使用單例類提供的公有工廠方法獲得單例對象,而不該該使用反射來建立,使用反射將會破壞單例模式 ,將會實例化一個新對象。併發

單線程實現方式

在單線程環境下,單例模式根據實例化對象時機的不一樣分爲,ide

  • 餓漢式單例(當即加載)餓漢式單例在單例類被加載時候,就實例化一個對象並將引用所指向的這個實例;
  • 懶漢式單例(延遲加載),只有在須要使用的時候纔會實例化一個對象將引用所指向的這個實例。

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


餓漢式單例

// 餓漢式單例
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實現懶漢式單例(線程安全)。
  • 枚舉式單例
各位看官還能夠嗎?喜歡的話,動動手指點個💗,點個關注唄!!謝謝支持!
歡迎掃碼關注,原創技術文章第一時間推出
相關文章
相關標籤/搜索