併發專題(四)單例模式

目錄

分類

餓漢式

懶漢式

Synchronized實現

DCL非線程安全的實現

DCL線程安全實現--volatile實現

基於ClassLoader的實現

基於枚舉的實現

分類

  • 懶漢式:懶漢式是指應用啓動時並不會初始化相應的實例,而是在第一次使用時加載,也就是所謂的延時加載吧,關於延時加載還有很多話聊,筆者就不一一談了。
  • 餓漢式:餓漢式是指應用啓動時就初始化相應的實例,可能說相對來說比較簡單。

餓漢式

先講講餓漢式,這個比較簡單,直接加載就可以了。直接上代碼:

public class Singleton {
    private static Singleton singleton = new Singleton();

    private Singleton(){}
    
    public static Singleton getInstance(){
        return singleton;
    }
}

也有人這樣寫,不過原理是一樣的,都是在類靜態初始化階段初始化實例:

public class Singleton {
    private static Singleton singleton;

    private Singleton(){}
    static {
        singleton = new Singleton();
    }
    public static Singleton getInstance(){
        return singleton;
    }
}

餓漢式沒過多可講的,下面我們分析一下懶漢式。

懶漢式

最簡單的實現

不多說,直接上代碼。

public class Singleton {
    private static Singleton singleton;

    private Singleton(){}
    
    public static Singleton getInstance(){
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

這個代碼在單線程環境下會良好運行,但在多線程環境下會有較大問題,也就是所謂的線程不安全。設想一下,線程A在運行到singleton = new Singleton()時,線程B剛好在進行singleton == null, 這時線程B會繼續進入if塊,而重新對線程A已經實例化的singleton進行重新實例化,這樣就衝突了,這還是簡單的兩個線程,如果是多個線程同時進行,那就比較嚴重了。
解決這個問題的最簡單方法是用同步塊synchronized

Synchronized實現

public class Singleton {
    private static Singleton singleton;

    private Singleton(){}
    
    public static synchronized Singleton getInstance(){
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

這個肯定是線程安全的,因爲整個方法都被鎖住了,但這樣解決了初始化實例的問題,卻導致了每次只能有一個線程調用該方法,其他線程都會被鎖住,這樣就會導致較大的性能損失。解決這個問題可以使用DCL(Double Check Lock)

DCL非線程安全的實現

public class Singleton {
    private static Singleton singleton;

    private Singleton(){}
    
    public static Singleton getInstance(){
        if (singleton == null){
            synchronized(Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

我們先分析一下這個代碼。

  • 只有實例第一次被訪問時,纔會有線程進入同步塊,這樣極大提高了性能。避免了synchronized帶來的較大性能損失。
  • 第一次訪問時,如果有多個線程同時進入if塊,只有第一個線程會獲得鎖,其他線程被阻塞,第一個線程可以創建實例。
  • 第一次訪問時,被阻塞的線程會進入同步塊,進行第二次check,如果此時實例不爲null,則返回。
    仔細一想,這個代碼挺完美的,但是不是這個樣子的,具體問題出現在哪呢?
    Java程序創建一個實例的過程爲:
  1. 分配內存空間
  2. 初始化對象
  3. 將內存空間的地址賦值給對應的引用
    但是由於指令重排的原因,什麼是指令重排?指令重排序是JVM爲了優化指令,提高程序運行效率。指令重排序包括編譯器重排序和運行時重排序。JVM規範規定,指令重排序可以在不影響單線程程序執行結果前提下進行。既然這樣,那麼在應用真正運行時可能是這個樣子的:
  4. 分配內存空間
  5. 將內存空間的地址賦值給對應的引用
  6. 初始化對象

線程執行順序

根據上圖分析可以看出new Singleton()時可能會導致錯誤。所以解決這個問題的方法:

  1. 禁止初始化階段的發生重排序
  2. 初始化階段可以發生重排序,但不能被其他線程“知道”

DCL線程安全實現--volatile實現

volatile是Java中的一個關鍵字,使用該關鍵字修飾的變量在被變更時會被其他變量可見。

public class Singleton {
    //通過volatile關鍵字來確保安全
    private volatile static Singleton singleton;

    private Singleton(){}

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

基於ClassLoader的實現

這個方案是利用ClassLoader本身的機制來避免多個線程同時實例化該變量。也就是解決的上面說的2. 初始化階段可以發生重排序,但不能被其他線程“知道”。

public class Singleton {
    private static class SingletonHolder{
        public static Singleton singleton = new Singleton();
    }
    
    public static Singleton getInstance(){
        return SingletonHolder.singleton;
    }
}

基於枚舉的實現


public enum DataSourceEnum {
    DATASOURCE;
    private DBConnection connection = null;
    private DataSourceEnum() {
        connection = new DBConnection();
    }
    public DBConnection getConnection() {
        return connection;
    }
}

其實Enum就是一個普通的類,它繼承自java.lang.Enum類
把上面枚舉編譯後的字節碼反編譯,得到的代碼如下:


public final class DataSourceEnum extends Enum<DataSourceEnum> {
      public static final DataSourceEnum DATASOURCE;
      public static DataSourceEnum[] values();
      public static DataSourceEnum valueOf(String s);
      static {};
}

由反編譯後的代碼可知,DATASOURCE 被聲明爲 static 的,根據在【單例深思】餓漢式與類加載 中所描述的類加載過程,可以知道虛擬機會保證一個類的<clinit>() 方法在多線程環境中被正確的加鎖、同步。所以,枚舉實現是在實例化時是線程安全。

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