單例模式:看了這篇文章,你能和麪試官暢談單例模式

一、前言

最近看了很多的書還有視頻,他們都花了很長的篇幅提到了單例模式,於是我想把他們都總結起來,寫下這篇文章。目的就是,讓小白能搞懂單例模式,以及單例模式的經典面試題。爲什麼說是小白也能懂的呢?哈哈哈,還不是小胖也是一個小白~~~

二、單例模式的解釋

單例模式定義:一個類只能有一個實例,且該類能自行創建這個實例的一種模式。其實單例模式在C#或者.NET裏面更好理解,像win7的任務管理器,在系統中只能創建一個。有些理解了嘛?

單例模式只能有一個實例,實例化其實就是new的過程,是不可能阻止他人不去用new的。所以我們完全可以直接就把這個類的構造方法改成私有的。對於外部的代碼,不能用new來實例化他,我們完全可以再寫一個public方法,叫做getInstance(),這個方法的目的就是返回一個實例,但是在這個方法中,我們需要是否實例化的判斷。
來一個簡單的例子

package singleton;

/**
 * 描述: 懶漢式(線程不安全)
 * **/
public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3(){

    }
    //如果兩個線程同時到達,會出現線程不安全的情況
    public static Singleton3 getInstance(){
        if (instance == null){
            instance = new Singleton3();
        }
        return instance;
    }
}

單例模式,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。可以節省內存和計算、保證結果正確、方便管理。適用場景是無狀態的工具類、全局信息類。

記住,上面的單例模式是線程不安全的。

三、實現單例模式的8種寫法

1.餓漢式(靜態常量)(可用)
package singleton;

/***
 * 描述:餓漢式(靜態常量)  (可用)
 * **/
public class Singleton1  {
    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1(){

    }
    public static Singleton1 getInstance(){
        return INSTANCE;
    }
}

上面是餓漢式的靜態常量的寫法,可以看到類在加載後就完成了實例化的創建。優點:寫法簡單,類加載後就完成了實例的創建。缺點:提前佔用系統的資源。

2.餓漢式(靜態代碼塊)(可用)
package singleton;

/***
 * 描述:餓漢式(靜態代碼塊)  (可用)
 * **/
public class Singleton2  {
    private final static Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2(){

    }
    public static Singleton2 getInstance(){
        return INSTANCE;
    }
}

上面是餓漢式的靜態代碼塊的方式,只不過和第一種有一些區別。優缺點和第一種是一樣的。優點:寫法簡單,類加載後就完成了實例的創建。缺點:提前佔用系統的資源。

3.懶漢式(線程不安全)(不可用)
package singleton;

/**
 * 描述: 懶漢式(線程不安全)
 * **/
public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3(){

    }
    //如果兩個線程同時到達,會出現線程不安全的情況
    public static Singleton3 getInstance(){
        if (instance == null){
            instance = new Singleton3();
        }
        return instance;
    }
}

上面的懶漢式的寫法,它是線程不安全的,因爲在多線程的時候,可能會有兩個線程同時到達instance==null,然後都會初始化,這就創建了兩個對象了,是線程不安全的,不能使用。

4.懶漢式(線程安全)(不推薦)
package singleton;

/**
 * 描述:懶漢式(線程安全)(不推薦)
 * */
public class Singleton4 {
    private  static  Singleton4 instance;
    private Singleton4(){

    }
    //但是效率不高
    public synchronized static Singleton4 getInstance(){
        if (instance == null){
            instance = new Singleton4();
        }
        return instance;
    }
}

上面是懶漢式的第二種寫法,這種方法是線程安全的,但是不推薦使用,因爲效率是低下的。在getInstance上面加上了synchronized的同步方法,那麼只能有一個線程可以進入到這個方法中,但是在多線程的時候效率是非常低的,因爲任何一個線程進入的這個方法,都需要去等待鎖的釋放,所以不推薦使用。

5.懶漢式(線程不安全)(不可用)
package singleton;

/**
 * 描述:懶漢式(線程不安全) (不推薦)
 * */
public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5(){

    }
    public static Singleton5 getInstance(){
        if (instance == null){
            synchronized (Singleton5.class){
                instance = new Singleton5();
            }
        }
        return instance;
    }
}

上面是懶漢式的第三種寫法,是對第二種的改進,不在鎖在方法上,加在創建對象上面,但是這可能會引發線程安全的問題。如果連個線程都判斷instance==null,都進入到if裏面,這時只有一個線程會運行,但是第一個線程執行完成之後,第二個線程還是會創建實例,那麼就是線程不安全的。

6.懶漢式(雙重檢查)(推薦面試使用)
package singleton;

/**
 * 描述: 雙重檢查
 * 優點: 線程安全;延遲加載;效率較高
 * 爲什麼要double-check
 * 1.線程安全
 * 2.單check爲什麼不行?
 * 3.放在判斷後面會引發線程安全問題
 * 4.單層鎖,但是synchronized放在方法上,這樣可以,但是會導致性能問題
 *
 * 爲什麼要用volatile
 * 1.新建對象實際上有3個步驟(分配內存資源,調用構造函數,將對象指向分配的內存空間)新建對象不是原子操作。
 * 2.JVM重排序會帶來NPE(空指針的問題)
 * 3.防止重排序
 * */
public class Singleton6 {
    private volatile static Singleton6 instance;
    private Singleton6(){

    }
    public static Singleton6 getInstance(){
        if (instance == null){
            synchronized (Singleton6.class){
                if (instance == null){
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}

上面就是很有名的double-check單例模式了,它的優點有:線程安全;延遲加載;效率較高。它是線程安全的,而且效率很高,推薦我們在面試的時候使用。這個代碼同時也引出了我們在面試過程中的2個問題。
懶漢式單例模式爲什麼要用double-check,不用就不安全嗎?懶漢式單例模式爲什麼雙重檢查模式要用volatile?

7.懶漢(靜態內部類方式)(可用)
package singleton;

/**
 * 描述: 靜態內部類方式,可用
 * 懶漢
 * ***/
public class Singleton7 {
    private Singleton7(){
    }
    private static class SingletonInstance{
        private static final Singleton7 INSTANCE = new Singleton7();
    }
    public static Singleton7 getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

上面是靜態內部類的方式,是可以推薦使用的,而且效率也是可以的。外部類加載,JVM不會創建多個實例。

8.枚舉 推薦在項目中使用
package singleton;

/**
 * 描述:單例模式:枚舉 推薦用
 * */
public enum Singleton8 {
    INSTANCE;

    public void whatever(){
    //無論什麼方法
    }
}

上面是枚舉方式的單例模式,是生產實踐中最佳的單例模式的寫法,同時可以防止反序列化破壞單例。

四、常見面試問題

什麼叫單例設計模式

答:單例模式的重點在於整個系統上共享一些創建時較耗資源的對象。整個應用只維護一個特定類實例,它被所有組件共同使用。Java.lang.Runtime是單例模式的經典例子。

你知道餓漢式的缺點嗎?

答:餓漢模式,類一加載的時候就會實例化對象,所以要提前佔用系統資源。

那懶漢式的缺點呢?

答:不會出現佔用資源的問題,但是需要使用合適,否則會帶來線程安全問題。

懶漢式單例模式爲什麼要用double-check,不用就不安全嗎?

答:爲了線程安全,我們需要使用double-check。

追問,單check爲什麼不行?(代碼見5.懶漢式)

答:單check是線程不安全的(代碼見5.懶漢式),可能會有多個線程走到了 if (instance == null)的裏面,由於synchronized看似只能有一個線程會創建對象,但是第二個也會創建。

追問,你可以把synchronized寫在方法的外面呀?(代碼見4,。懶漢式)

答:這個是可以解決線程安全的問題,但是效率不是很高,每個線程都需要等待鎖的釋放,會導致性能的問題,不推薦使用。

你說爲什麼要用volatile?

答:在多線程的時候,創建對象分爲3步,CPU可能會重排序,首先建一個空的對象,然後複製給引用,然後調用構造方法。
第一個線程進來了,第一個對象已經不是空的,但是構造方法沒有執行,裏面的屬性是空的。第二個線程發現不是空的,就會直接跳過創建實例的方法,之後再使用的時候引發的問題。使用volatile可以避免這個問題,對於第二個線程來說,他的創建過程對第一個線程來說是可見了,他就會等待創建完成。

那這麼多應該如何選擇,用哪種單例的實現方案最好?

答:枚舉方式的單例模式,是生產實踐中最佳的單例模式的寫法,同時可以防止反序列化破壞單例。

那你知道happens-before原則嘛?

答:volatile就是happens-before原則呀…未完待續

請用Java寫出線程安全的單例模式。

答:上面已經很多例子了,小胖覺得可以用第6種double-check。

五、關於幾種解法的選擇

《劍指offer》上面的推薦給面試官的解法是1.餓漢式(靜態常量)(可用)和7.懶漢(靜態內部類方式)(可用)。
《大話設計模式》上面的推薦也是1.餓漢式(靜態常量)(可用)
《線程八大核心+Java併發底層原理精講》上面推薦使用6.懶漢式(雙重檢查)(推薦面試使用)

小胖覺得可以使用第6種,這可能會打開一些面試的問題,把問題引入到我們瞭解熟悉的方向。當然你在手寫單例模式的時候,可以去詢問一下要求,是需要餓漢式還是懶漢式。

六、參考資料

書籍1:《大話設計模式》 第21章 有些類也需計劃生育–單例模式
書籍2:《劍指offer》 面試題2:實現Singleton模式
視頻:《線程八大核心+Java併發底層原理精講》

七、關於本系列的解釋

本系列想製作23種設計模式+7種設計原則一系列課程,其目的就是一個簡單的記錄學習的過程。不知道能幫助到多少人,也不知道技術是否會有一定的深度。

製作不易,您的點贊是我最大的動力。希望我們都能成爲想成爲的人

希望我們都能成爲想成爲的人

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章