單例模式就是如此簡單

在面試中相信不少人會被問到:說說你最瞭解的三個設計模式,平常開發中使用過哪些設計模式等等。最近幾篇文章就來學習一下設計模式,這是第一篇文章,也是最多見的模式——單例模式。java

什麼是單例

單例模式(Singleton Pattern),顧名思義,即保證一個類僅有一個實例,並在全局中提供一個訪問點。git

在實現單例時,要保證一個類僅有一個實例,就不能提供公有的構造方法,任由其餘類建立實例,對應變量也須要爲 static,只在加載時初始化一次。另外呢,要在全局中都能訪問到,還須要提供一個靜態的公有方法來進行訪問。github

具體實現方式比較多,對於不一樣的場景,也應該選擇不一樣的方式,例如是否須要保證線程安全,是否須要延遲加載。下面具體來看一下。面試

餓漢式(線程安全)

根據上面對單例模式實現的說明,能夠很容易地想到以下實現:設計模式

public class Singleton1 {

    private static Singleton1 instance = new Singleton1();

    private Singleton1() { }

    public static Singleton1 getInstance() {
        return instance;
    }
}
複製代碼

這種方式在該類第一次被加載時,就會建立好該實例。這就是所謂的餓漢式,也就是,在想要使用實例時,馬上就能拿到,而不須要進行等待。安全

另外這種方式,由 JVM 保證其線程安全。可是這種方式可能會形成資源消耗,由於有可能這個實例根本就用不到,而進行沒必要要的加載。多線程

懶漢式(非線程安全)

上述方式在類加載時就進行實例化,可能會形成沒必要要的加載。那麼咱們能夠在其真正被訪問的時候,再進行實例化,因而能夠寫出以下方式:ide

public class Singleton2 {

    private static Singleton2 instance;

    private Singleton2() { }

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

getInstance 方法中,第一次訪問時,因爲沒有初始化,纔去進行進行初始化,在後續訪問時,直接返回該實例便可。這就是所謂的懶漢式,也就是,它不會提早把實例建立出來,而是將其延遲到第一次被訪問的時候。性能

可是懶漢式存在線程安全問題,以下圖:學習

在多線程場景下,若是有兩個線程同時進入 if 語句中,則這兩個線程分別建立了一個對象,在兩個線程從 if 中退出時,就建立了兩個不同的對象。

懶漢式(線程安全)

既然普通的懶漢式會出現線程安全問題,那麼給建立對象的方法加鎖便可:

public class Singleton3 {

    private static Singleton3 instance;

    private Singleton3() { }

    public static synchronized Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}
複製代碼

上述這種作法雖然在多線程場景下也能正常工做,也具有延遲加載。但因爲 synchronized 方法鎖住了整個方法,效率比較低。因而,聰明的小夥伴,能夠很容易想到,使用同步方法塊,來減少加鎖的粒度。

看下面兩種作法,加鎖粒度確實減少了,可是它們卻並不能保證線程安全:

synchronized (Singleton4.class) {
    if (instance == null) {
        instance = new Singleton4();
    }
}
複製代碼

因爲指定重排序出現問題,後面介紹雙重校驗鎖時會詳細說。

if (instance == null) {
    synchronized (Singleton4.class) {
        instance = new Singleton4();
    }
}
複製代碼

若是 synchronized 加在 if 語句外面,這和普通的懶漢式作法同樣,沒有區別。若是有兩個線程分別進入 if 語句,雖然也有加鎖操做,可是兩個線程都會執行實例化,也就是會進行兩次實例化。

雙重校驗鎖(線程安全)

因而引出了雙重校驗鎖方式,能夠先判斷對象是否實例化,若是沒有再進行加鎖,再加鎖以後,再次判斷是否實例化,若是仍然沒有實例化,才實例化對象。

這種作法的完整代碼以下:

public class Singleton5 {
    
    private static volatile Singleton5 instance;
    
    private Singleton5() { }
    
    public static Singleton5 getInstance() {
        // 若是已經實例化,則直接返回,不用加鎖,提高性能
        if (instance == null) {
            synchronized (Singleton5.class) {
                // 再次檢查,保證線程安全
                if (instance == null) {
                    instance = new Singleton5();
                }
            }
        }
        return instance;
    }
}
複製代碼

能夠看到,在 synchronized 語句先後,有兩個 if 判斷,這就是所謂的雙重校驗鎖。

使用 volatile

其實,若是僅僅是雙重校驗的話,仍然不能保證線程安全問題。這就要分析 instance = new Singleton5(); 這段代碼。

雖然代碼只有一句,但在 JVM 中它其實被分爲三步執行:

  1. instance 分配內存空間;
  2. instance 進行初始化;
  3. instance 指向分配的內存地址;

但因爲編譯器或處理器可能會對指令重排序,執行的順序就有可能變成 1->3->2。這在單線程環境下不會出現問題,可是在多線程環境下可能會致使一個線程得到尚未初始化的實例。

例如,線程 A 執行了第 13 步後,此時線程 B 調用 getInstance() 方法,判斷 instance 不爲空,所以返回 instance。但此時 instance 還未被初始化。

因此,就須要使用 volatile 關鍵字來修飾 instance,禁止編譯器的指令重排序,保證在多線程環境下也能正常運行。

靜態內部類式(線程安全)

目前雙重校驗鎖的作法看起來不錯,使用延遲加載,在保證線程安全的同時,加鎖粒度也比較小,效率還不錯。那還有沒有其餘方法呢?

那就是使用靜態內部類來實現,來看一下它的實現:

public class Singleton6 {

    private Singleton6() { }

    private static class InnerSingleton {
        private static final Singleton6 INSTANCE = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return InnerSingleton.INSTANCE;
    }
}
複製代碼

在這種實現中,當外部類 Singleton6 類被加載時,靜態內部類 InnerSingleton 並無被加載。

而是隻有當調用 getInstance 方法,從而訪問類的靜態變量時,纔會加載內部類,從而實例化 INSTANCE。而且 JVM 能確保 INSTANCE 只能被實例化一次,即它也是線程安全的。

枚舉式(線程安全)

另外,使用枚舉實現單例也是一種不錯的方式,代碼很是簡單:

public enum Singleton6 {
    INSTANCE();

    Singleton6() { }
}
複製代碼

枚舉的實現中,類被定義爲 final,其枚舉值被定義爲 static final,對枚舉值的初始化放在靜態語句塊中。因此,對象在該類第一次被加載時實例化,這不只避免了線程安全問題,並且也避免了下面提到的反序列化對單例的破壞。

單例與序列化

如今來看一下,對象在序列化和反序列化時,是否還可以保證單例。

這裏使用雙重校驗鎖實現的單例類,對 Singleton5 類添加 Serializable 接口,而後進行測試:

public class SingletonTest {

    public static void main(String[] args) {
        Singleton5 instance1 = Singleton5.getInstance();
        try ( ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")) ){
            oos.writeObject(instance1);
        } catch (IOException e) {
            e.printStackTrace();
        }

        Singleton5 instance2 = null;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("tempFile"))) ){
            instance2 = (Singleton5) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println(instance1 == instance2);
    }
}
// false
複製代碼

能夠看到,對 Singleton5 進行反序列獲得的是一個新的對象,如此就破壞了 Singleton5 的單例性。

咱們能夠在 Singleton5 類中添加一個 readResolve() 方法,並在該方法中指定要返回的對象的生成策略:

public class Singleton5 implements Serializable {

    private static volatile Singleton5 instance;

    private Singleton5() { }

    public static Singleton5 getInstance() {
        if (instance == null) {
            synchronized (Singleton5.class) {
                if (instance == null) {
                    instance = new Singleton5();
                }
            }
        }
        return instance;
    }

    // 添加 readResolve 方法
    private Object readResolve() {
        return instance;
    }
}
複製代碼

經過 debug 方法查看源碼,在 readObject 方法的調用棧中,能夠看到 ObejctStreamClass 類的 invokeReadResolve 方法:

若是定義了 readResolve 方法,會經過反射進行調用,根據指定的策略來生成對象。

有哪些好的單例模式實踐

JDK#Runtime

該類用於獲取應用運行時的環境。能夠看到這是一個餓漢式的單例。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
}
複製代碼

Spring#Singleton

Spring 中定義 Bean 時,能夠指定是單例仍是多例(默認爲單例):

@Scope("singleton")
複製代碼

查看其源碼,單例模式實現以下:

public abstract class AbstractFactoryBean<T> implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {

    private T singletonInstance;
    
    @Override
	public void afterPropertiesSet() throws Exception {
	    // 掃描配置時,單例模式
	    // 就會將 initialized 置爲 true
		if (isSingleton()) {
			this.initialized = true;
			// 調用子類方法建立對象
			this.singletonInstance = createInstance();
			this.earlySingletonInstance = null;
		}
	}
    
    @Override
	public final T getObject() throws Exception {
		if (isSingleton()) {
			return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
		}
		else {
			return createInstance();
		}
	}
}
複製代碼

參考資料

相關文章
相關標籤/搜索