【java 設計模式】 單例模式精解(面試再也不用怕了)

前言

這篇 博客是跑更問底的學習單例模式,看了本博客,對於一般的面試官,你都可以手撕了,但是大神級別的面試官,後邊還會補充。

一、手寫單例模式

1、餓漢式
public class Singleton {
    // 構造方法私有化,其他類就不能通過new的方式來創造對象
    private Singleton(){
    }
    // 內部提供一個當前的實例,必須要靜態化,因爲下面的靜態方法要調用
    private static Singleton singleton=new Singleton();
    // 提供公共的靜態方法,返回當前類的對象,外部類調用的唯一路徑
    public static Singleton getInstance(){
        return singleton;
    }
}
2、懶漢式
public class Singleton {
    private Singleton() {
    }
    private static Singleton singleton = null;
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
3、對比分析

首先餓漢式,就是迫不及待的new 出一個對象,然後不管你調不調用,我都要new 出一個對象,這樣雖說是線程安全的,但是對象加載的時間長,耗費內存。 懶漢式,因爲它是懶加載,什麼時候用,什麼時候new 對象,延時了對象的創建,節省內存空間,但是它是線程不安全的。如果不知道爲啥是線程不安全的,還請看我之前寫的多線程的系列博客。

4、jdk中單例模式應用舉例

jdk中的RunTime 就是餓漢式的 ,如下:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

二、餓漢式線程不安全的解決方式

(1)synchronized關鍵字實現線程同步
public class Singleton {
    private Singleton() {
        System.out.println("hahahaha");
    }
    private static Singleton singleton = null;
    public  static  Singleton getInstance() {
        synchronized (Singleton.class){
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }
}

上面的代碼效率稍差,先不提synchronized()重量級鎖的事情,是因爲當singleton不是null的時候,其他線程每次進來的時候都要去判斷有沒有Singleton.class這個鎖,效率差在了這裏,我們稍微改進一下,用雙端檢索機制(DCL (double check lock))進行修改.

(2) DCL 方式提高效率
public class Singleton {
    private Singleton() {
        System.out.println("hahahaha");
    }

    private static Singleton singleton = null;

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

到這裏你就覺得完美了,重點來了,上面的雙端監測的方式也不一定是線程安全的,因爲還有指令重排的存在。這會導致某一個線程執行到第一次檢測,讀取到的singleton不爲null時,singleton的引用對象可能沒有完成初始化

因爲singleton=new Singleton();可以分爲三步
僞代碼

memory=allocate(); //1、分配對象內存空間
singleton(memory);//2、初始化對象
singleton=memory;//3、設置singleton指向剛分配的內存地址,此時instance!=null

步驟2和步驟3不存在數據依賴關係.而且無論重排前還是重排後程序執行的結果在單線程中並沒有改變,因此這種重排優化是允許的.

memory=allocate();//1.分配對象內存空間
singleton=memory;//3.設置singleton指向剛分配的內存地址,此時instance!=null 但對象還沒有初始化完.
singleton(memory);//2.初始化對象

由上可知,當一條線程訪問singleton不爲null時,由於存在singleton實例未完成初始化的可能性,此時就造成了線程安全問題。

既然是指令重排導致的問題,我們前邊介紹了volatile關鍵字的作用了, 看一看這篇博客 https://blog.csdn.net/jerry11112/article/details/106870835,其中volatile可以保證原子性,禁止指令重排,所以我們加上volatile關鍵字就好

public class Singleton {
    private Singleton() {
    }

    private static volatile Singleton singleton = null;

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

三、單例模式——應用場景

1、應用程序的日誌應用,一般都使用單例模式實現,這一般是由於共享的日誌文件一直處於打開狀態,因爲只有一個實例去操作,否則內容不好追加。
2、數據庫連接池
3、網站計數器
4、Spring中的單例模式,Spirng bean有一個屬性爲scope 其中默認就是singleton 就是單例模式

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