線程安全的單例模式分析

本文針對於單例模式中對象創建中的線程安全問題。主要以懶漢式,餓漢式,靜態內部類,枚舉類分析在調用時創建對象的線程安全問題。


1.餓漢式

類加載會導致該單實例對象被創建

    // 問題1:爲什麼加 final
    // 問題2:如果實現了序列化接口, 還要做什麼來防止反序列化破壞單例
public final class HungrySingleton implements Serializable {
    // 問題3:爲什麼設置爲私有? 是否能防止反射創建新的實例?
    private HungrySingleton(){}
    // 問題4:這樣初始化是否能保證單例對象創建時的線程安全?
    private static final HungrySingleton INSTANCE = new HungrySingleton();
    // 問題5:爲什麼提供靜態方法而不是直接將 INSTANCE 設置爲 public, 說出你知道的理由
    public static HungrySingleton getInstance(){
        return INSTANCE;
    }

    private Object readResolve(){
        return INSTANCE;
    }

}

問題回答:

  1. 用final修飾的類不能被擴展,也就是說不可能有子類。若被繼承子類可能修改父類的內部結構會破壞單例模式
  2. 重寫readResolve()並返回單例對象;readResolve() 讀取序列保存的變量值,導致在反序列化的時通過字節碼會重新生成實例導致單例被破壞
  3. 私有化創建方法導致外部不能夠調用構造方法直接創建實例;不能夠防止反射創建實例,Co
    constructor.setAccessible(true);即可訪問私有構造函數
  4. 線程安全;成員變量是在類加載的時候完成初始化,類加載的階段有JVM保證線程安全
  5. 方法可以靈活封裝並且可以提供對泛型的支持(個人看法)

2.懶漢式

類加載不會導致該單實例對象被創建,而是首次使用該對象時纔會創建

private Singleton() { }
 private static Singleton INSTANCE = null;
 // 分析這裏的線程安全, 並說明有什麼缺點
 public static synchronized Singleton getInstance() {
 if( INSTANCE != null ){
 return INSTANCE;
 } 
 INSTANCE = new Singleton();
 return INSTANCE;
 }

問題:
synchronized同步代碼塊鎖範圍過大,導致調用初始化方法每次都需要加鎖解鎖,存在效率問題


3.枚舉類

// 問題1:枚舉單例是如何限制實例個數的
// 問題2:枚舉單例在創建時是否有併發問題
// 問題3:枚舉單例能否被反射破壞單例
// 問題4:枚舉單例能否被反序列化破壞單例
// 問題5:枚舉單例屬於懶漢式還是餓漢式
// 問題6:枚舉單例如果希望加入一些單例創建時的初始化邏輯該如何做
enum Singleton { 
 INSTANCE; 
}

問題回答:

  1. 該答案通過字節碼來進行說明
public final enum com/lock/singleton/Singleton extends java/lang/Enum  {

  // compiled from: Singleton.java

  // access flags 0x4019
  public final static enum Lcom/lock/singleton/Singleton; INSTANCE

INSTANCE爲枚舉類中的靜態成員變量肯定是單實例
2.靜態成員變量在類加載器創建對象時已經由JVM創建完成,不存在併發問題
3.答案是不能的

@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //用於判斷是否有Enum修飾
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
  1. 不能被序列化
  2. 在類加載時創建實例變量,屬於餓漢式
  3. 以下是我個人一些實現
public class EnumSingleton {
    //私有化構造函數
    private EnumSingleton() {
    }
    //定義一個靜態枚舉類
    static enum EnumSingletonClass {
        //創建一個枚舉對象,該對象天生爲單例
        INSTANCE;
        private EnumSingleton enumSingleton;
        //私有化枚舉的構造函數
        private EnumSingletonClass() {
            enumSingleton = new EnumSingleton();
        }
        public EnumSingleton getInstance() {
            return enumSingleton;
        }

    }
    //對外暴露一個獲取對象的靜態方法
    public static EnumSingleton getInstance() {
        return EnumSingletonClass.INSTANCE.getInstance();
    }
}

DCL( double-checking lock) 懶漢單例

public final class Singleton {
 private Singleton() { }
 // 問題1:解釋爲什麼要加 volatile ?
 private static volatile Singleton INSTANCE = null;
 
 // 問題2:對比實現3, 說出這樣做的意義 
 public static Singleton getInstance() {
 if (INSTANCE != null) { 
 return INSTANCE;
 }
 synchronized (Singleton.class) { 
 // 問題3:爲什麼還要在這裏加爲空判斷, 之前不是判斷過了嗎
 if (INSTANCE != null) { // t2 
 return INSTANCE;
 }
 INSTANCE = new Singleton(); 
 return INSTANCE;
 } 
 }
}

問題回答:


靜態內部類懶漢單例

public class StaticInnerSingleton {

    private StaticInnerSingleton(){}

    // 問題1:屬於懶漢式還是餓漢式
    private static class LazyHolder{
       static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
    }
    // 問題2:在創建時是否有併發問題
    public static StaticInnerSingleton getInstance(){
        return LazyHolder.INSTANCE;
    }
}

問題回答:

  • 1.懶漢式,因爲類加載屬於懶加載只有當使用的該類時纔會創建。
  • 2.不會有併發問題,當外部調用getInstance,會觸發類加載並由JVM創建實例,所以保證了線程安全問題。

以上爲個人學習總結,若有不準備之處還望指正,共勉!

這世上總有太多不期而遇的溫暖,支撐着我們走過每個難熬的瞬間

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