淺談Java單例模式

相信在設計模式中有一個經常提到的概念:單例模式,爲什麼它經常出現在面試話題中,因爲它的應用場景十分廣泛。

使用場景:

比如

  • 數據庫連接池,作爲數據庫的緩存,避免頻繁連接關閉數據庫,
  • Java線程池,控制管理線程。
  • log4j日誌記錄,由始至終記錄着運行日誌。

定義:

保證系統中一個類只有一個實例,而且必須自己創建自己的唯一實例,該實例易於外界訪問,從而方便對實例個數的控制並節約系統資源。


創建單例模式的幾種方式以及比較

1. 餓漢模式

/*
 1. 餓漢模式:
 2. 優點:線程安全
 3. 缺陷:性能低/加載類就初始化單例/不適合需要外部傳入參數配置的單例模式
 */
public class SingletonHungry {

    private static final SingletonHungry instance = new SingletonHungry();

    public static SingletonHungry getInstance() {
        return instance;
    }

    public static void main(String[] args) {        

        SingletonHungry s1 = SingletonHungry.getInstance();
        SingletonHungry s2 = SingletonHungry.getInstance();
        System.out.print("餓漢模式實例對比:");
        //true
        System.out.println(s1.getInstance()==s2.getInstance());
    }   

}

由於餓漢模式在類內部創建實例,所以它是線程安全,正式它在類內部就靜態加載,所以它不能從外部傳入參數配置。
具體來看看懶漢模式.

2. 懶漢模式

package com.dd.code.singleton;

/*
 * 懶漢模式
 * 優點:簡單/對比餓漢,加載此單例可以外部傳入配置
 * 缺陷:線程不安全
 */
public class SingletonLazy {

    private static SingletonLazy instance;

    /*  
     * 配置成員conf(假設必須傳入conf該單例纔可以加載)
     *  這不能在類中優先初始化 private static final SingletonHungry instance = new SingletonHungry();
     */
    private static String conf;


    //外部傳入屬性配置
    public static void setConf(String conf) {
        SingletonLazy.conf = conf;
    }

    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }


    public static void main(String[] args) {
        SingletonLazy.setConf("配置文件優先");
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.print("懶漢模式實例對比:");
        //true
        System.out.println(s1.getInstance()==s2.getInstance());
    }

}

對比餓漢模式,懶漢模式可以傳入必要配置再手動實例化,但是由於手動實例化,則需要考慮線程安全問題。

3. 線程安全懶漢模式

/*
 *  線程安全懶漢加載
 *  優點:線程安全/可以外部傳配置
 *  缺陷:代價較高,創建單例只需要第一次保證線程安全就好,不需要每次都同步
 *  優化解決:SingletonDoubleCheck
 */
public class SingletonThreadSafe {
    private static SingletonThreadSafe instance;

    //加了同步關鍵字synchronized
    private synchronized SingletonThreadSafe getInstance() {
        if (instance == null) {
            instance = new SingletonThreadSafe();
        }
        return instance;
    }

    public static void main(String[] args) {
        SingletonThreadSafe s1 = new SingletonThreadSafe();
        SingletonThreadSafe s2 = new SingletonThreadSafe();
        System.out.print("線程安全懶漢加載實例對比:");
        //true
        System.out.println(s1.getInstance()==s2.getInstance());
    }
}

加了同步關鍵字synchronized保證了線程安全,但是它的性能就降低了,而且其實創建單例只需要第一次保證線程安全就好,不需要每次都同步。

所以引入了新的優化,好像很厲害的雙重鎖檢測模式

4. 雙重鎖檢測單例


/*
 * 雙重鎖單例模式(較複雜)
 * 優點:可傳入配置/對比SingletonThreadSafe性能優化,只有在實例爲空(第一次實現同步)
 * 爲什麼要第二次判斷instance是否爲空,因爲把synchronized放裏層的話,
 * 有可能有多個線程進入了*臨界區*,synchronized只能保證臨界區每次由一個線程執行而已,
 * 二次檢測可以讓其他線程下次不初始化,防止冗餘情況
 */
public class SingletonDoubleCheck {

    /*
     * volatile關鍵字:
     * 要知道,instance = new SingletonDoubleCheck();不是原子性操作,
     * 雖然volatile關鍵字不能保證原子性,但是可以禁止指令重排
     * 保證了instance = new SingletonDoubleCheck() 這一行的有效執行順序 
     */
    private volatile static SingletonDoubleCheck instance;


    private static SingletonDoubleCheck getInstance() {
        if (instance == null) {
            //非臨界區
            synchronized (SingletonDoubleCheck.class) {
                //*臨界區*
                if (instance == null) {
                    instance = new SingletonDoubleCheck();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        SingletonDoubleCheck s1,s2;
        s1 = SingletonDoubleCheck.getInstance();
        s2 = SingletonDoubleCheck.getInstance();
        System.out.print("雙重鎖單例加載實例對比:");
        //true
        System.out.println(s1==s2);
    }

}

雙重鎖模式可以說是性能和線程安全的折中,它保證線程安全又保證不需要每次都控制同步,第一個判斷if (instance == null)用來攔截已經創建的線程。
主要複雜的是從非臨界區到臨界區的情況,即當未創建實例的時候:要知道synchronized關鍵字其實只能保證每次一個線程執行修飾代碼塊,並不能保證只有一個線程,假設有超過一個線程進入臨界區,此時如果線程一執行instance = new SingletonDoubleCheck(),則線程2下一次會根據第二個if (instance == null)判斷是否再次創建實例,所以第二個if其實相當於一個flag標記,它巧妙的避免了多個線程創建多個實例


這種方式有點燒腦,官方推薦還有另一種創建方式,靜態內部類模式,比較推薦使用的一種

5. 靜態內部類

/*
 * 靜態內部類 
 */
public class SingletonNested {

    private static class SingletonHolder{
        public static final SingletonNested HOLDER_INSTANCE = new SingletonNested();
    }

    public static SingletonNested getInstance() {
        return SingletonHolder.HOLDER_INSTANCE;
    }

    public static void main(String[] args) {
        SingletonNested s1, s2;
        s1 = SingletonNested.getInstance();
        s2 = SingletonNested.getInstance();
        System.out.print("靜態內部類實例對比:");
        //true
        System.out.println(s1==s2);
    }
}

靜態內部類創建單例的優點:

  1. 由jvm保證線程安全,不需要用到同步控制,性能較高;
  2. 因爲區別於懶加載把instance作爲靜態內部成員,所以類加載時不會實例化instance 只有getInstance調用纔會初始化。

參考鏈接:

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