1. 程式人生 > >【趣味設計模式系列】之【單例模式】

【趣味設計模式系列】之【單例模式】

1. 簡介

單例模式(Singleton):確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項。

2. 圖解

類圖如下:

3. 案例實現

單例特點:

  • 外部類不能隨便對單例類建立,故單例的構造方法必須為private,在類的內部自行例項化;
  • 提供一個public方法入口,作為唯一呼叫單例類的途徑得到例項。

3.1 餓漢式

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 12:41
 * @Desc: 餓漢式
 *  類載入到記憶體後,就例項化一個單例,JVM保證執行緒安全
 *  唯一缺點:不管用到與否,類裝載時就完成例項化
 */
public class HungrySingleton {

    private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    /**
     * 私有構造方法,只有本類才能呼叫
     */
    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }

    public static void main(String[] args) {
        HungrySingleton h1 = HungrySingleton.getInstance();
        HungrySingleton h2 = HungrySingleton.getInstance();
        System.out.println(h1 == h2);
    }
}

執行結果:

true
  • 分析:類載入到記憶體,就例項化一個單例,通過final的靜態變數保證唯一例項。
  • 優點:執行緒安全。
  • 缺點:不管是否用到,類裝載時就完成例項化。

3.2 懶漢式

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 12:50
 * @Desc: 懶漢式-執行緒不安全
 */
public class LazySingleton {

    private static LazySingleton LAZY_SINGLETON;

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (null == LAZY_SINGLETON) {
            //睡眠1毫秒,在new物件前,增加被其他執行緒打斷的機會,保證能被多個執行緒執行
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LAZY_SINGLETON = new LazySingleton();
        }

        return LAZY_SINGLETON;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(LAZY_SINGLETON.getInstance().hashCode())
            ).start();
        }
    }
}

執行結果

1338303845
1276043710
874014640
2116194883
835777993
527405373
38860249
772510047
484927678
1375039115
.
.
.
  • 分析:懶載入在呼叫getInstace方法的時候建立例項,通過100個執行緒測試發現其hashcode並不相等,並不是單例。
  • 優點:改善了餓漢式中例項不用也載入的弊端。
  • 缺點:引入了新的問題,執行緒不安全,並不能保證單例

3.3 synchronized修飾方法單例

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 13:50
 * @Desc: 懶漢式-執行緒安全
 * 通過synchronized解決,但獲取鎖帶來效能開銷,效率下降
 */
public class ThreadSafeSingleton {

    private static ThreadSafeSingleton LAZY_SINGLETON;

    private ThreadSafeSingleton() {
    }

    public static synchronized ThreadSafeSingleton getInstance() {
        if (null == LAZY_SINGLETON) {
            //睡眠1毫秒,在new物件前,增加被其他執行緒打斷的機會,保證能被多個執行緒執行
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LAZY_SINGLETON = new ThreadSafeSingleton();
        }

        return LAZY_SINGLETON;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(LAZY_SINGLETON.getInstance().hashCode())
            ).start();
        }
    }
}

執行結果

2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
.
.
.
  • 分析:通過synchronized關鍵字保證執行緒安全。
  • 優點:執行緒安全,保證單例。
  • 缺點:方法上加鎖導致效能開銷。

3.4 synchronized修飾程式碼塊的單例

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 13:50
 * @Desc: 懶漢式-執行緒不安全
 *  試圖通過減小同步程式碼塊的方式提高效率,帶來了執行緒不安全
 */
public class ThreadUnSafeSingleton {

    private static ThreadUnSafeSingleton LAZY_SINGLETON;

    private ThreadUnSafeSingleton() {
    }

    public static  ThreadUnSafeSingleton getInstance() {
        if (null == LAZY_SINGLETON) {
            //試圖通過減小同步程式碼塊的方式提高效率,帶來了執行緒不安全
            synchronized (ThreadUnSafeSingleton.class) {
                //睡眠1毫秒,在new物件前,增加被其他執行緒打斷的機會,保證能被多個執行緒執行
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LAZY_SINGLETON = new ThreadUnSafeSingleton();
            }

        }

        return LAZY_SINGLETON;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(LAZY_SINGLETON.getInstance().hashCode())
            ).start();
        }
    }
}

結果:

1276043710
1276043710
1276043710
1276043710
1276043710
1276043710
2116194883
2116194883
2116194883
2116194883
.
.
.
  • 分析:試圖通過減小同步程式碼塊的方式提高效率,帶來了執行緒不安全。
  • 優點:減小了加鎖的範圍,提高了效能。
  • 缺點:執行緒不安全,結果顯示不能保證單例。

3.5 雙重檢測安全單例

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 13:50
 * @Desc: 雙重檢測下執行緒安全
 *
 */
public class DoubleCheckedThreadSafeSingleton {
    
    private volatile static DoubleCheckedThreadSafeSingleton LAZY_SINGLETON;

    private DoubleCheckedThreadSafeSingleton() {
    }

    public static DoubleCheckedThreadSafeSingleton getInstance() {
        if (null == LAZY_SINGLETON) {
            //試圖通過減小同步程式碼塊的方式提高效率,帶來了執行緒不安全
            synchronized (DoubleCheckedThreadSafeSingleton.class) {
                if (null == LAZY_SINGLETON) {
                    //睡眠1毫秒,在new物件前,增加被其他執行緒打斷的機會,保證能被多個執行緒執行
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    LAZY_SINGLETON = new DoubleCheckedThreadSafeSingleton();
                }
            }

        }

        return LAZY_SINGLETON;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(LAZY_SINGLETON.getInstance().hashCode())
            ).start();
        }
    }
}

執行結果

527405373
527405373
527405373
527405373
527405373
527405373
527405373
527405373
.
.
.
  • 分析:第一個判空語句if (null == LAZY_SINGLETON),用來檢測如果記憶體中有單例生成以後,永不進入下面的程式碼,直接走return語句返回已有的單例,第二個判空語句,保證當前執行緒拿到鎖的前後,記憶體中都沒有單例,才執行建立單例操作,防止中途被其他執行緒建立單例,進而重複建立;volatile關鍵字保證在執行語句LAZY_SINGLETON = new DoubleCheckedThreadSafeSingleton() 時,可以分解為如下的3行虛擬碼。
memory = allocate();  // 1:分配物件的記憶體空間
ctorInstance(memory); // 2:初始化物件
instance = memory;  // 3:設定instance指向剛分配的記憶體地址

上面3行虛擬碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的)。2和3之間重排序之後的執行時序如下。

memory = allocate();  // 1:分配物件的記憶體空間
instance = memory;  // 3:設定instance指向剛分配的記憶體地址
// 注意,此時物件還沒有被初始化!
ctorInstance(memory); // 2:初始化物件

上面的程式碼在編譯器執行時,可能會出現重排序 從1-2-3 排序為1-3-2,如果發生重排序,另一個併發執行的執行緒B就有可能在判斷instance不為null。執行緒B接下來將訪問instance所引用的物件,但此時這個物件可能還沒有被A執行緒初始化!

  • 優點:保證單例與執行緒安全。
  • 缺點:增加了程式碼的複雜度。

3.6 內部靜態類單例

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 15:45
 * @Desc:  靜態內部類方式
 *  JVM保證單例
 *  載入外部類時不會載入內部類,這樣可以實現懶載入
 */
public class InnerStaticClassSingleton {

    private InnerStaticClassSingleton() {

    }

    //內部靜態類
    private static class InnerStaticClassSingletonHolder {
        private static final InnerStaticClassSingleton INSTANCE = new InnerStaticClassSingleton();
    }

    public static InnerStaticClassSingleton getInstance() {
        return InnerStaticClassSingletonHolder.INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(InnerStaticClassSingleton.getInstance().hashCode())
            ).start();
        }
    }
}

執行結果:

772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
.
.
.
  • 分析:因為虛擬機器載入類的時候只加載一次,並且載入外部類時不會載入內部類,只有呼叫getInstance方法的時候,內部類才被載入,所以內部類的靜態變數也只加載一次,JVM保證了單例與執行緒安全。
  • 優點:執行緒安全。
  • 缺點:無。

3.7 列舉單例

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 16:05
 * @Desc: 列舉實現,既可以保證執行緒安全,還可以防止反序列化。
 */
public enum EnumSingleton {
    INSTANCE;
    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(EnumSingleton.INSTANCE.hashCode());
            }).start();
        }
    }
}
  • 優點:執行緒安全,同時保證不被反序列化,因為列舉型別沒有構造方法,不能反序列化後建立物件。
  • 缺點:寫法優點怪異。

4. 框架原始碼分析

以下原始碼分析基於Spring5.0.6 RELEASE。DefaultSingletonBeanRegistry.class部分原始碼如下.

    @Nullable
    public Object getSingleton(String beanName) {
        return this.getSingleton(beanName, true);
    }

    @Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // 從單例快取容器中載入 bean
        Object singletonObject = this.singletonObjects.get(beanName);
        // 單例快取容器中的 bean 為空,且當前 bean 正在建立
        if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
            // 加鎖
            synchronized(this.singletonObjects) {
                // 從 earlySingletonObjects 容器中獲取
                singletonObject = this.earlySingletonObjects.get(beanName);
                // earlySingletonObjects容器中沒有,且允許提前建立
                if (singletonObject == null && allowEarlyReference) {
                    // 從 singletonFactories 中獲取對應的 ObjectFactory
                    ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                    // ObjectFactory 不為空,則建立 bean
                    if (singletonFactory != null) {
                        // 獲取 bean
                        singletonObject = singletonFactory.getObject();
                        // 新增 bean 到 earlySingletonObjects 中
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        // 從 singletonFactories 中移除對應的 ObjectFactory
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }

        return singletonObject;
    }
  • 第一步,從singletonObjects 中,獲取 Bean 物件。
  • 第二步,若為空且當前bean正在建立中,則從earlySingletonObjects中獲取Bean物件。
  • 第三步,若為空且允許提前建立,則從singletonFactories中獲取相應的ObjectFactory物件。若不為空,則呼叫其ObjectFactorygetObject(String name)方法,建立Bean物件,然後將其加入到earlySingletonObjects ,然後從singletonFactories刪除。
    由此可見,Spring在建立單例bean的時候,採用的是雙重檢測加鎖機制建立bean的。

5. 單例與靜態方法的比較

  • 單例支援延遲載入,靜態類第一次載入就初始化;
  • 單例常駐記憶體,除非JVM退出,靜態方法中的物件,會隨著靜態方法執行完被釋放,gc回收。

6. 應用場景

  • 資料庫連線池,因為頻繁建立或者關閉資料庫連線,損耗效能非常大,因為何用單例模式來維護,就可以大大降低這種損耗;
  • 執行緒池,因為執行緒是一種稀缺資源,頻繁建立執行緒,會導系統開銷增大,執行緒之間的頻繁切換也導致效能下降,由統一的執行緒池管理執行緒;
  • 開發中常用的配置工具類,因為配置類是共享的資源;
  • 日誌應用,因為日誌屬於工享檔案,一直處於開啟狀態,因為只能有一個例項去操作,否則內容不好追加;
  • Windows的工作管理員,多次開啟只會彈出一個對話方塊,確保系統由一個工作管理員管理;
  • 網站的計數器,如果多個,計數難以同步;
    綜上,單例應用在共享資源上,要麼方便管理,要麼節約效能,避免不必要的效能開銷。

7. 總結

單例的具體寫法,需要結合場景與業務要求,確認是否支援執行緒安全,是否支援延遲載入,單例比較簡單的寫法是餓漢式,唯一的不足是不支援懶載入,還有靜態內部類;比較完美的寫法是雙重檢測加鎖,雖然寫法複雜,但支援延遲載入,執行緒安全,也是Spring原始碼使用的方式