天天看點

單例模式(萬字長文精講)

1、單例模式的定義

單例模式(Singleton Pattern),確定一個類隻有一個執行個體,并提供對它的全局通路點。這是在java-design-patterns.com中對于單例模式的定義,其原文定義如下:

Ensure a class has only one instance, and provide a global point of acess to it.

簡單來說就是確定系統中隻建立特定類的一個對象(全文的重點和圍繞展開的都是如何安全的更高效的去将類的執行個體化限制為一個對象)。

2、單例模式的簡單使用

在深入分析單例模式之前,先簡單的有個整體的概念,單例模式大緻是怎麼使用的。我們通過一個簡單的demo和其UML類圖來做個介紹。

單例模式通用寫法:

單例模式(萬字長文精講)
單例模式(萬字長文精講)

3、單例模式的優缺點和使用場景

在很多技術的學習和選擇的前置條件我想可能都會是這兩個問題為先驅,是以把本該放置到最後去總結的兩個點,優先提到了最前面,也正是出于這個原因。單例模式的編碼說簡單确實簡單,編碼一個最優的單例或者如何更好的适用目前場景,對編碼人員有一定的要求,如下主要分析其優缺點和使用場景,便于編碼人員的選擇。

3.1 單例模式的優點

隻建立一個執行個體對象,減少記憶體開銷,同時也減少了GC

隻建立一個示例對象,通過全局唯一的通路點,可以避免對資源的多重占用,例如檔案的對同一個檔案的寫

通過這個全局通路點為抓手,可以對資源的通路做優化處理,例如通路點中預置緩存

3.2 單例模式的缺點

單例模式的功能一般寫于一個類中,這回導緻業務邏輯耦合,如果設計不合理,是違背單一職責原則

單例模式一般不實作接口,使其擴充困難,是違背開閉原則

單例模式隻存在一個對象,在并發測試中不利于調試

3.3 使用典型場景

日志類,對于日志的記錄等操作,我們通常使用單例模式

資料庫連接配接和通路的管理

檔案資源通路管理

生成唯一的序列号

系統全局唯一的通路端點,例如系統通路請求計數器

對象的建立需要大量的開銷或者對象會被頻繁調用可以考慮通過單例模式來優化替換

3.4 源碼中的使用

說三道四不如看看Java最佳規範JDK中對于單例模式的一些使用

java.lang.Runtime

單例模式(萬字長文精講)
單例模式(萬字長文精講)

除了在jdk源碼中對于單例模式有不少的使用之外,我們最常見的Spring架構中,每個Bean預設就是單例的,這樣做的原因在于Spring容器可以管理Bean的生命周期。修改Spring中Bean的單例屬性,隻需要設定為Prototype類型即可,這樣Bean的生命周期将不再有Spring容器跟蹤。

4、餓漢式單例

餓漢式單例主要展現在一個餓字,也就是說在使用這個對象之前,在類加載的時候就立即初始化,建立其執行個體對象。這樣做的好處是線程安全、使用時速度更快。餓漢

寫法一:

單例模式(萬字長文精講)
單例模式(萬字長文精講)

餓漢式單例可以保證線程安全,執行效率高,同時編碼簡單容易了解。但是餓漢式單例隻适用于單例對象減少的情況,如果大量編寫餓漢式單例不僅會給系統啟動帶來負擔,也可能會導緻記憶體的浪費,比如建立的單例對象在程式運作過程中并未使用。是以這對記憶體浪費這個問題,我們衍生出了懶漢式單例。

5、懶漢式單例

懶漢式單例主要展現在一個懶字,也就是說類加載的時候我懶得執行個體化,等你需要用了再說吧!接下來的懶漢式單例中我通過一步步的優化和推翻來演進如何編寫一個優秀的單例。

5.1 普通懶漢式單例

單例模式(萬字長文精講)

這是一個非常普通的懶漢式單例模式的寫法,相信很多初學者會編碼成這樣,但其實這是一種錯誤的線程不安全的寫法。我們通過一個斷點測試來證明其不安全,我采用的是IDEA編寫代碼,通過設定斷點為Thread模式來使得線程1和線程2同時滿足LAZY_SINGLETON == null。

測試代碼

單例模式(萬字長文精講)
單例模式(萬字長文精講)
單例模式(萬字長文精講)
單例模式(萬字長文精講)
單例模式(萬字長文精講)
單例模式(萬字長文精講)

如上代碼看似兼顧了性能和同步對象的執行個體化,但是這裡的同步語言并不能保證對象隻執行個體化一次,它隻能保證每次隻有一個線程在執行個體化這個對象。試想一下,如果線程1和線程2均執行到代碼21行,此時線程1獲得鎖繼續執行,執行個體化對象LazySingletonDemo4,當線程1退出鎖後,線程2擷取到鎖,線程2也會對LazySingletonDemo4進行執行個體化,這種情況也可以用上面的斷點法測試出來。是以這種情況是錯誤的,但是隻有小小的改動一下即可,改動後的方式如下所示。

5.3 雙重檢查鎖懶漢式單例

雙重檢查鎖看名字高大上,其實就是在上面的LazySingletonDemo4多個if判斷而已,了解為雙重檢查+鎖可能更明了,其代碼如下所示:

單例模式(萬字長文精講)

這三條指令中,指令2和指令3可能會出現重排序,也就是說對象的初始化會被後置到将配置設定的位址指向對象的引用這個指令的後面;這種重排序,假設是線上程1執行中發生了,此時線程2執行到第20行 if (LAZY_SINGLETON == null),此時LAZY_SINGLETON 并不為null,程式會直接傳回LAZY_SINGLETON對象,但是此時的對象是一個執行個體化不完全的對象,這種情況是不允許存在的。

其解決辦法是通過volatile關鍵字借助其記憶體語義來禁止指令重排序,這樣2和3指令之間的重排序将會被禁止,這涉及到JMM規範,需要的請檢視我的并發專題系列文章。其改造代碼如下所示:

package com.lizba.pattern.singleton.lazy;

單例模式(萬字長文精講)
單例模式(萬字長文精講)
單例模式(萬字長文精講)
單例模式(萬字長文精講)

5.4 靜态内部類懶漢式單例

在雙重檢查鎖的演進中,我們通過不斷的縮小鎖的範圍,以及對對象是否未執行個體化做了兩次判斷,最後對反射擷取對象這種操作做了處理;但是歸根到底,雙重檢查鎖中有個鎖字,就難規避性能讨論的問題,其實這種問題在大部分場景中是可以接受;如果硬要尋求一種既是懶漢式,又不需要鎖的單例模式,那麼通過靜态内部類的加載特性,巧妙的實作懶漢式單例模式會是一種不錯的選擇。

Java語言中的内部類是延時加載的,隻有在第一次使用的時候才會被加載,不使用則不加載。

我們通過該特性,編碼的懶漢式單例如下所示:

package com.lizba.pattern.singleton.lazy;

/**
 * <p>
 *      内部類懶漢式單例
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/27 20:14
 */
public class LazySingletonDemo8 {

    private LazySingletonDemo8() {
        if (LazyHolderInner.LAZY_SINGLETON != null) {
            throw new RuntimeException("This operation is forbidden.");
        }
    }

    public static LazySingletonDemo8 getInstance() {
        return LazyHolderInner.LAZY_SINGLETON;
    }

    /**
     *  使用内部類,被使用才加載的特性來
     */
    private static class LazyHolderInner {
      public static final LazySingletonDemo8 LAZY_SINGLETON = new LazySingletonDemo8();
    }

}
      
單例模式(萬字長文精講)
單例模式(萬字長文精講)

更加上述輸出結果,可以非常明确的發現,instance1和instance2就是同一個對象,那麼為何枚舉可以實作單例呢?為何其被Joshua Bloch大師稱為最好的單例模式編碼方式呢?

我們通過反編譯工具jad來一探究竟,首先使用jad,在EnumSingletonDemo.class所在的目錄下執行jad EnumSingletonDemo.class,這樣會生成一個EnumSingletonDemo.jad檔案;

單例模式(萬字長文精講)

生成的EnumSingletonDemo.jad檔案内容如下:我們看到代碼的最後有一個靜态代碼塊,在這裡一看便知。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumSingletonDemo.java

package com.lizba.pattern.singleton.hungry;


public final class EnumSingletonDemo extends Enum{

    public static EnumSingletonDemo[] values(){
        return (EnumSingletonDemo[])$VALUES.clone();
    }

    public static EnumSingletonDemo valueOf(String name){
        return (EnumSingletonDemo)Enum.valueOf(com/lizba/pattern/singleton/hungry/EnumSingletonDemo, name);
    }

    private EnumSingletonDemo(String s, int i) {
        super(s, i);
    }

    public Object getObject()  {
        return object;
    }

    public void setObject(Object object){
        this.object = object;
    }

    public static EnumSingletonDemo getInstance() {
        return SINGLETON_INSTANCE;
    }

    public static final EnumSingletonDemo SINGLETON_INSTANCE;
    private Object object;
    private static final EnumSingletonDemo $VALUES[];

    // 靜态代碼塊中執行個體化了EnumSingletonDemo
    static  {
        SINGLETON_INSTANCE = new EnumSingletonDemo("SINGLETON_INSTANCE", 0);
        $VALUES = (new EnumSingletonDemo[] {
            SINGLETON_INSTANCE
        });
    }
}
      
單例模式(萬字長文精講)
單例模式(萬字長文精講)