單例模式(單例模式作用、常見形式、代碼實現)

 

一、單例模式能幹啥?

所謂單例,就是整個程序有且僅有一個實例。

某個類全局只有一個實例對象有什麼好處?一方面,由於單例模式只生成一個實例,減少了系統性能開銷;另一方面,單例模式存在全局訪問點,所以可以優化共享資源訪問。比如:網站的計數器,一般也是採用單例模式實現,如果存在多個計數器對象,每一個用戶的訪問都刷新不同的計數器對象的值,統計總數的時候就很麻煩。如果採用單例模式實現就不會存在這樣的問題,而且還可以避免線程安全問題。還有很多這樣的場景:

  1. Windows的任務管理器
  2. Windows的回收站,也是一個單例應用
  3. 項目中的讀取配置文件的對象(如Mybatis中的Configuration類的對象,保存配置信息)
  4. 數據庫的連接池
  5. Servlet中的Application Servlet
  6. Spring中的Bean默認也是單例的
  7. SpringMVC Struts中的控制器

總的來說適用於:

  • 1.需要生成唯一序列的環境
  • 2.需要頻繁實例化然後銷燬的對象。
  • 3.創建對象時耗時過多或者耗資源過多,但又經常用到的對象。 
  • 4.方便資源相互通信的環境

二、單例模式常見形式

總的按對象的創建時機,可以分爲懶漢式和餓漢式:

1.餓漢式:線程安全 調用率高 但是不能延遲加載
2.懶漢式:線程安全 調用率不高 但是可以延遲加載
3.雙重檢測(double check )-----懶漢式
4.靜態內部類(線程安全 可以延遲加載)--------懶漢式
5.枚舉單例 線程安全 不可以延遲加載

看着形式很多,實際上單例模式都有以下特點:

  1. 構造器私有:這個很好理解,要是構造器不私有,那就可以在外部隨意創建不同的對象了,違背了單例思想
  2. 持有自己類型的屬性
  3. 對外提供獲取實例的靜態方法

三、懶漢式

懶漢式顧名思義,比較懶,只在你需要的時候才創建這個唯一的對象。它分爲兩種:

懶漢式(1)

public class Singleton1 {
    // 自己持有自己
    private static Singleton1 instance;
    // 構造器私有化,不讓外部通過構造器產生對象,從而保證對象全局唯一
    private Singleton1() {}
    // 對外提供獲取唯一實例的靜態方法
    public static Singleton1 getInstance() {
        // 先判斷實例對象是否已經存在
        if(instance == null){
            // 創建實例
            instance = new Singleton1();
        }
        return instance;
    }
}

上面的如果是單線程的情況下是沒問題的,但是在多線程時就有可能產生多個不同的對象,即是線程不安全的。

 

懶漢式(2)

上面懶漢(1)線程不安全,因此在創建實例方法上加上鎖就有了下面的

public class Singleton2 {
    // 自己持有自己
    private static Singleton2 instance;
    // 構造器私有化,不讓外部通過構造器產生對象,從而保證對象全局唯一
    private Singleton2() {}
    // 對外提供獲取唯一實例的靜態方法
    public static synchronized Singleton2 getInstance() {
        // 先判斷實例對象是否已經存在
        if(instance == null){
            // 創建實例
            instance = new Singleton2();
        }
        return instance;
    }
}

因爲鎖的原因,效率自然不高。 

四、餓漢式

 餓漢式顧名思義,比較飢渴,不管你要不要這個對象都先給你創建出來。

public class Singleton3 {
    // 自己持有自己並直接創建對象
    private static Singleton3 instance = new Singleton3();
    // 構造器私有化,不讓外部通過構造器產生對象,從而保證對象全局唯一
    private Singleton3() {}
    // 對外提供獲取唯一實例的靜態方法
    public static Singleton3 getInstance() {
       return instance;
    }
}

和懶漢式的延時創建相比,餓漢式在加載類的時候對象就已經創建了,所以加載類的速度比較慢,但是獲取對象的速度比較快,調用效率高,且是線程安全的。

五、雙檢鎖式(DCL)

上面的懶漢式(2)加了鎖雖然線程安全了,但是效率也降低了,而雙檢鎖式可以兼顧線程安全和效率。實際上,雙檢鎖也可以看成是懶漢式的一種特殊形式,也是延時創建唯一的對象。

public class Singleton4 {
    // 自己持有自己並直接創建對象(使用volatile關鍵字防止重排序,new Instance()是一個非原子操作,可能創建一個不完整的實例)
    private static volatile Singleton4 instance;
    // 構造器私有化,不讓外部通過構造器產生對象,從而保證對象全局唯一
    private Singleton4() {}
    // 對外提供獲取唯一實例的靜態方法
    public static Singleton4 getInstance() {
        // 判斷是否存在單例
        if(instance == null){
            // 加鎖,保持只有一個線程執行(只需在第一次創建實例時才同步)
            synchronized (Singleton4.class){
                // 再次判斷單例是否被創建(防止其他線程已經創建而導致再次創建)
                if (instance == null){
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }
}

這裏有兩次對象的判空處理,鎖是在兩次判空中間的,這個鎖只會在第一次創建實例的時候執行一次,一旦該實例被創建後面的線程就不會再需要拿到這個鎖,因此不會因爲鎖而降低效率。

注意:

這裏還有個關鍵字volatile,它是來防止某個線程獲取不完整對象的。new Instance()是一個非原子操作,jvm存在亂序執行功能,因爲重排序的原因,可能創建出來的就是一個不完整的實例。比如演示一下new Instance()過程:

  1. 分配實例所需的內存
  2. 創建引用,指向這個內存
  3. 初始化實例對象

這是一種順序,但這個順序不是固定的,實際過程中也可能是下面的順序

  1. 分配實例所需的內存
  2. 初始化實例對象
  3. 創建引用,指向這個內存

每次執行順序是重排序的,那就可能發生下面的現象:

  1. 線程A先進入Singleton4()方法
  2. 此時instance == null,線程A進入synchronized 塊
  3. 再次判斷,此時instance == null,線程A在執行 instance = new Singleton4();時,先執行“分配實例所需的內存”和“創建引用指向這個內存”,但還沒有初始化這個對象
  4. 此時線程B進來,因爲instance雖然未被初始化,但已經非null,不會再進行下面的創建語句,直接返回這個未初始化的instance
  5. 線程A等到資源後,繼續完成對象初始化操作,線程A獲得完成的instance對象

上面的線程B就拿到了非完整對象,這應該是個重大bug,volatile 關鍵字修飾這個對象可以避免以上重排序可能帶來的問題(volatile 需要在JDK1.5之後的版本才能確保安全)。

 

六、靜態內部類式 

/**
 * Feng, Ge 2020/2/29 14:14
 */
public class Singleton5 {
    // 構造器私有化,不讓外部通過構造器產生對象,從而保證對象全局唯一
    private Singleton5() {}

    // 靜態內部類
    private static class SingleInnerHolder{
        private static Singleton5 instance = new Singleton5();
    }

    // 對外提供獲取唯一實例的靜態方法
    public static Singleton5 getInstance() {
        return SingleInnerHolder.instance;
    }
}

你肯定覺得這不是餓漢式麼,對象直接就創建了,實際這個對象也是延時創建的,因此也可以理解成是懶漢式的一種特殊形式。

外部類加載時並不需要立即加載內部類,內部類不被加載則不去初始化INSTANCE,故而不佔內存。

這裏當Singleton5類被加載時,其靜態內部類SingletonHolder沒有被主動使用,只有當調用getInstance方法時, 纔會裝載SingletonHolder類,從而實例化單例對象。 

這樣的方式既能實現懶漢式對象的延時創建,也保證了線程的安全。

那麼,靜態內部類又是如何實現線程安全的呢?首先,我們先了解下類的加載時機。
類加載時機:JAVA虛擬機在有且僅有的5種場景下會對類進行初始化:

  1. 遇到new、getstatic、setstatic或者invokestatic這4個字節碼指令時,對應的java代碼場景爲:new一個關鍵字或者一個實例化對象時、讀取或設置一個靜態字段時(final修飾、已在編譯期把結果放入常量池的除外)、調用一個類的靜態方法時。
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒進行初始化,需要先調用其初始化方法進行初始化。
  3. 當初始化一個類時,如果其父類還未進行初始化,會先觸發其父類的初始化。
  4. 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的類),虛擬機會先初始化這個類。
  5. 當使用JDK 1.7等動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

這5種情況被稱爲是類的主動引用,注意,這裏《虛擬機規範》中使用的限定詞是"有且僅有",那麼,除此之外的所有引用類都不會對類進行初始化,稱爲被動引用。靜態內部類就屬於被動引用的行列。

我們再回頭看下getInstance()方法,調用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler裏的INSTANCE對象,跟上面那個DCL方法不同的是,getInstance()方法並沒有多次去new對象,故不管多少個線程去調用getInstance()方法,取的都是同一個INSTANCE對象,而不用去重新創建。當getInstance()方法被調用時,SingleTonHoler纔在SingleTon的運行時常量池裏,把符號引用替換爲直接引用,這時靜態對象INSTANCE也真正被創建,然後再被getInstance()方法返回出去,這點同餓漢模式。那麼INSTANCE在創建過程中又是如何保證線程安全的呢?在《深入理解JAVA虛擬機》中,有這麼一句話:

 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個進程阻塞(需要注意的是,其他線程雖然會被阻塞,但如果執行<clinit>()方法後,其他線程喚醒之後不會再次進入<clinit>()方法。同一個加載器下,一個類型只會初始化一次。),在實際應用中,這種阻塞往往是很隱蔽的。

故而,可以看出INSTANCE在創建過程中是線程安全的,所以說靜態內部類形式的單例可保證線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。

那麼,是不是可以說靜態內部類單例就是最完美的單例模式了呢?其實不然,靜態內部類也有着一個致命的缺點,就是傳參的問題,由於是靜態內部類的形式去創建單例的,故外部無法傳遞參數進去,例如Context這種參數,所以,我們創建單例時,可以在靜態內部類與DCL模式裏自己斟酌。

 

七、枚舉式

public enum Singleton6 {
    // 定義1個枚舉的元素,即爲該類的唯一實例
    INSTANCE;

    public void anyMethod(){
        System.out.println("任何一個方法!");
    }
}
/**
 * Feng, Ge 2020/2/29 15:34
 */
public class TestEnum {
    public static void main(String[] args) {
        Singleton6 singleton6 = Singleton6.INSTANCE;
        singleton6.anyMethod();
    }
}

枚舉在java中與普通類一樣,都能擁有字段與方法,防止反序列化生成多個實例,在任何情況下,它都是一個單例,因此線程安全。

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