「設計模式」- 教你手寫單例模式

這是我參與8月更文挑戰的第12天,活動詳情查看: 8月更文挑戰java

前言

單例模式(Singleton pattern):確保一個類只有一個實例,並提供該實例的全局訪問點安全

本文主要分析單例模式常見的幾種實現方式markdown


一. 類圖

單例模式使用一個私有構造函數、一個私有靜態變量以及一個公有靜態函數來實現。多線程

私有構造函數保證了不能經過構造函數來建立對象實例,只能經過公有靜態函數返回惟一的私有靜態變量。函數


二. 實現方式

2.1 餓漢式

餓漢式在類加載的時候就進行實例化,這樣作的好處是線程安全;但缺點也是有的,首先在加載的時候就進行實例化,萬一這個類佔用的資源很大,就會很是浪費資源,畢竟它不必定在何時被使用,但內存是一開始就被佔用了。post

public class HungryManSingleton {
    private static HungryManSingleton hungryManSingleton = new HungryManSingleton();

    private HungryManSingleton() { }
    
    public static HungryManSingleton getInstance() {
        return hungryManSingleton;
    }
} 
複製代碼

在main方法中驗證餓漢式實現的單例模式spa

HungryManSingleton instance1 = HungryManSingleton.getInstance();
HungryManSingleton instance2 = HungryManSingleton.getInstance();
System.out.println("從餓漢單例獲取的兩個實例比較:" + instance1.equals(instance2));
複製代碼

輸出:線程


使用反射破壞餓漢式單例模式:code

//使用反射獲取構造方法,再將構造方法的私有性破壞,而後用這個構造方法建立一個實例
Class<HungryManSingleton> singletonClass = HungryManSingleton.class;
Constructor<HungryManSingleton> declaredConstructor = singletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);

HungryManSingleton instance1 = HungryManSingleton.getInstance();
HungryManSingleton instance2 = declaredConstructor.newInstance();
System.out.println("與反射獲取的實例比較:" + instance2.equals(instance1));
複製代碼

輸出:orm

能夠看到,他們並非同一個對象,這意味着餓漢式單例模式被破壞了

事實上,使用反射後,不管是餓漢式、懶漢式、升級的雙重校驗鎖機制、靜態內部類機制,都是不安全的


2.2 懶漢式

在懶漢式的實現中,默認不會進行實例化,何時用到了,何時 New,從而節約資源

public class LazySingleton {
    private static LazySingleton lazySingleton;
    
    private LazySingleton() {
        System.out.println(Thread.currentThread().getName());
    }

    public static LazySingleton getInstance() {
        if (lazySingleton == null) lazySingleton = new LazySingleton();
        return lazySingleton;
    }
}
複製代碼

可是這個實如今多線程的環境下是不安全的,試想如下,當 lazySingleton 爲空時,試想一下,當lazySingleton 爲空時,有多個線程同時經過了if (lazySingleton == null) 的判斷,這樣就會致使new 被執行了屢次,使用代碼復現一下:

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        new Thread(() -> LazySingleton.getInstance()).start();
    }
}
複製代碼

控制檯輸出:

能夠看到,實例化代碼被執行了三次,爲了解決線程安全的問題有兩個方法:

  1. getInstance() 方法的層級上加關鍵字 synchronized
  2. 引入雙重檢測鎖

3.3 雙重校驗鎖

爲了解決懶漢式線程不安全的問題,能夠引入雙重校驗鎖的機制,雙重檢驗鎖也是一種延遲加載,而且較好的解決了在確保線程安全的時候效率低下的問題

如下是代碼實現:

public class DCLSingleton {
    private volatile static DCLSingleton dclSingleton;

    private DCLSingleton() { }

    public static DCLSingleton getInstance() {
        if (dclSingleton == null) {
            synchronized (DCLSingleton.class) {
                if (dclSingleton == null) dclSingleton = new DCLSingleton();
            }
        }
        return dclSingleton;
    }
}
複製代碼

在這個實現中,對比一下懶漢式在方法上加鎖,那麼每次調用那個方法都要得到鎖,釋放鎖,等待等待……而雙重校驗鎖鎖住了部分的代碼。進入方法若是檢查爲空才進入同步代碼塊,這樣很明顯效率高了不少

3.3.1爲何要雙重校驗

那在這裏爲何 dclSingleton == null 要判斷兩次,假設咱們先去掉第二次的判斷。

若是兩個線程一塊兒調用 getInstance()方法,而且都經過了第一次的判斷 dclSingleton == null,那麼第一個線程獲取了鎖,而後進行了實例化後釋放了鎖,而後第二個線程會開始執行,而後立刻也進行了實例化,這就尷尬了。

因此加上第二次判斷後,先進來的線程判斷了一下,哦?爲空,我建立一個,而後建立一個實例以後釋放了鎖,第二個線程進來以後,哎?已經有了,那我就不用建立了,而後釋放了鎖,開開心心的完成了單例模式。


3.3.2 爲何要使用關鍵字volatile

對於 new 操做來講,它不是一個原子性操做,他在底層大概發生瞭如下三件事:

  • 在堆中分配內存空間
  • 執行它的構造方法,初始化對象
  • 在棧中定義引用,再把這個對象指給堆中的實際對象

咱們指望它是按順序發生的,可是因爲Java的指令重排機制,可能在沒有初始化對象時,就把棧中定義的引用指給堆中的空間,當第二個線程再進來的時候,第一次斷定是否爲空,他認爲不爲空,因而將尚未進行初始化的對象返回了;這就是爲何要加上關鍵字volatile的緣由。


3.4 靜態內部類實現

InnerClassSingleton類加載時,靜態內部類 InnerClass沒有被加載進內存。只有當調用 getInstance() 方法從而觸發 InnerClass.INSTANCEInnerClass纔會被加載,初始化實例 INSTANCE。

這種方式不只具備延遲初始化的好處,並且由虛擬機提供了對線程安全的支持。

public class InnerClassSingleton {
    private InnerClassSingleton() { }

    public static InnerClassSingleton getInstance() {
        return InnerClass.INSTANCE;
    }

    static class InnerClass {
        private static final InnerClassSingleton 
                INSTANCE = new InnerClassSingleton();
    }
}
複製代碼

3.5 枚舉

這是單例模式的最佳實踐,它實現簡單,而且在面對複雜的序列化或者反射攻擊的時候,可以防止實例化屢次

外部調用直接使用 Singleton.INSTANCE,簡單粗暴。

因爲 Enum 實現了 Serializable 接口,因此不用考慮序列化的問題(其實序列化反序列化也能致使單例失敗的,可是咱們這裏不過多研究),而且加載的時候 JVM 能確保只加載一個實例,因此它是線程安全的,並且反射沒法破解這種單例模式的實現

public enum Singleton {
    INSTANCE;
}
複製代碼

總結

本文論述了單例模式常見的五種實現方式,在《Effect Java》中,做者極力推崇使用枚舉類來實現單例模式,並認爲這個實現是單例模式的最佳實踐

感謝閱讀,但願本文對你有所幫助

相關文章
相關標籤/搜索