死磕設計模式-單例

單例模式,多是惟一一個咱們談到時,每一個工程師都會二眼放光,口若懸河的模式,除了它最簡單直接外,還由於咱們「自覺得」對它瞭如指掌,這篇文章帶你們作個總結,死磕單例模式的方方面面。java

單例的需求由來

大概有如下二種場景須要單例安全

  • 有些對象只應該存在一個,好比 「上帝」 「女媧」等等,自然具備單一的性質
  • 咱們造出來的配置類,管理類,好比UserManager,ServiceManager等等,只需維護一處,能夠全局引用

單例的四種實現

實現一個單例,咱們要考慮如下幾點。bash

  • 如何防止外部調用new關鍵字來建立新的對象
  • 如何作到防止經過對象序列化來建立新的對象
  • 實現了cloneable的類,如何防止clone來建立新的對象
  • 如何防止反射調用構造器來建立新的對象
  • 如何作到線程安全

總結一句話,如何線程安全的建立惟一實例對象。 先看一下Java中如何具體實現單例。app

單例實現-懶漢模式

public class UserManager {
    private static UserManager instance = new UserManager();
    private UserManager() { }
    public static UserManager getInstance() {
        return instance;
    }
}
複製代碼

首先經過私有化構造器,禁止了外部new的可能性,而後instance是static修飾的,因此在類被首次加載後,調用init 的時候,instance會被初始化,JVM保證類加載過程的線程安全,因此instance也是線程安全的。 由於在類加載初始化的時候,單例就被建立出來了,因此相對於按需延時加載,這種寫法若是有大量單例須要建立,在系統剛啓動時內存壓力比較大。同時上面的寫法也沒有可以禁止序列化和反射對單例的破壞(關於這個咱們放到最後來解決)。ide

單例實現-Double Check

private static volatile UserManager instance;

    private UserManager() {}

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

複製代碼

這也是很經典的單例實現,經過二次判空檢查,並且只有在第一次初始化時getInstance會加鎖,後面的獲取都不會加鎖,時間和空間效率都很高。 這裏要注意的一點是instance必定要加volatile修飾符。關於這一點,不少同窗可能會理解的不夠全面,下面我來詳細分析一下。 首先由於在建立UserManager的時候,咱們是有加鎖的,並且鎖的對象是UserManager這個Class對象。好比線程A得到了鎖,開始new UserManager(), 而且賦值給了instance,這時候線程B開始調用getInstance()來獲取單例對象,因爲鎖擁有可見性,因此線程A的賦值happen-before線程B的獲取,表面上看一切很完美,可是在jdk1.5以前,volatile語意尚未被增強,不能禁止指令重排序。ui

instance = new UserManager();
複製代碼

這條語句,其實能夠被看作三條僞代碼。spa

  • alloc userManager (堆上分配內存)
  • userManager init (對象初始化)
  • instance = userManager 注意,alloc必須首先執行,可是init 和第三條 賦值語句,JVM並無作定義,也就是說若是不加volatile,它們能夠被重排序。 一旦被重排序,線程B在獲取instance時,有可能獲取到的instance尚未執行init,這就是一次很危險的調用。可是加上volatile關鍵字,在jdk1.5以後,就不會再有這個問題了。

單例實現-靜態內部類

private UserManager() {}
    
    private static UserManager getInstance() {
        return SingltonHolder.sInstance;
    }

    private static class SingltonHolder {
        private static UserManager sInstance = new UserManager();
    }
複製代碼

靜態內部類的方式實現的單例一樣是線程安全的,由JDK來保證。同時也具備延時加載的特性。這種寫法對比Double-Check更簡潔,推薦使用。線程

單例實現-枚舉

Effective Java中推薦使用枚舉的方式來實現單例,咱們來看一下code

public enum UserManager {
    INSTANCE;
}
複製代碼

很簡潔,但咱們知道,枚舉是Java提供的語法糖,咱們解語法糖看下它的具體代碼對象

public final class com.dig.deep.design.singlton.UserManager extends java.lang.Enum<com.dig.deep.design.singlton.UserManager> {
  public static final com.dig.deep.design.singlton.UserManager INSTANCE;
  private static final com.dig.deep.design.singlton.UserManager[] $VALUES;
  public static com.dig.deep.design.singlton.UserManager[] values();
  public static com.dig.deep.design.singlton.UserManager valueOf(java.lang.String);
  private com.dig.deep.design.singlton.UserManager();
  static {};
}
複製代碼

能夠看到解語法糖後的UserManager,構造器也是私有的,有個一個static final 的INSTANCE類常量,能夠大膽猜想,JVM在加載枚舉類時,會給全部的枚舉項賦值,同時會保證過程的線程安全。

如何防止單例被破壞

咱們上面有提到過,一個完整的單例須要作到防止

  • 對象序列化對單例語意的破壞
  • 反射對單例語意的破壞
  • clone對單例語意的破壞

解決對象序列化的問題

try {
            UserManager userManager = UserManager.instance;
            FileOutputStream fileOutputStream = new FileOutputStream("user");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(userManager);

            FileInputStream fileInputStream = new FileInputStream("user");
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            UserManager newUserManager = (UserManager) objectInputStream.readObject();

            System.out.println("is equal: " + (userManager == newUserManager));
        } catch (IOException e) {
            e.printStackTrace();
        }
複製代碼

輸出是false,通過序列化和反序列化後,生成了二個單例對象,顯然破壞了單例的語意,解決這個問題,咱們能夠給UserManager增長一個readResolve方法, 並在其中返回單例對象。

private Object readResolve() {
        return instance;
    }
複製代碼

解決反射的問題

Class clazz = UserManager.class;
        Constructor[] constructors = clazz.getDeclaredConstructors();
        try {
            constructors[0].setAccessible(true);
            UserManager newUserManager = (UserManager) constructors[0].newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
複製代碼

若是開發者真的使用反射來做惡,誰能攔得住呢?雖然反射最終調用的仍是咱們的私有構造器,在構造器裏面咱們能夠加一些判斷邏輯,可是仍是不能涵蓋全部的狀況,由於畢竟咱們的單例實現多種多樣,有延時加載的,有非延時加載的。 可是經過Enum方式實現的單例是不可以被反射的,若是嘗試反射Enum的構造器,會拋出一個異常,因此Enum方式實現的單例對反射安全。

解決clone的問題

儘可能不要給單例實現cloneable接口,若是非要實現,也在重寫的clone方法裏,返回此單例對象。

@Override
    protected Object clone() throws CloneNotSupportedException {
        return getInstance();
    }
複製代碼

總結

單例模式比較簡單,同時咱們平常工做也用的很頻繁,工程師有必要對它有個全面瞭解,在選擇實現方案時作到心中有數。

相關文章
相關標籤/搜索