深入分析Java單例模式的各種方案


單例模式

Java內存模型的抽象示意圖:

Java內存模型的抽象示意圖

所有單例模式都有一個共性,那就是這個類沒有自己的狀態。也就是說無論這個類有多少個實例,都是一樣的;然后除此者外更重要的是,這個類如果有兩個或兩個以上的實例的話程序會產生錯誤。

非線程安全的模式

public class Singleton {
  private static Singleton instance;
  private Singleton(){
  }
  public static Singleton getInstance() {
    if (instance == null) //1:A線程執行
      instance = new Singleton(); //2:B線程執行
    return instance;
  }
}

普通加鎖

public class SafeLazyInitialization {
    private static Singleton instance;

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

出於性能考慮,采用雙重檢查加鎖的模式

雙重檢查加鎖模式

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

  }

  public static Singleton getInstance(){
    if(null == singleton){  //第一次檢查
      synchronized(Singleton.class){  //加鎖
        if(null == singleton){  //第二次檢查
          singleton = new Singleton();//問題的根源出在這里
        }
      }
    }
    return singleton;
  }
}

雙重檢查加鎖模式相對於普通的單例和加鎖模式而言,從性能和線程安全上來說都有很大的提升和保障。然而雙重檢查加鎖模式也存在一些隱蔽不易被發現的問題。首先我們要明白在JVM創建新的對象時,主要要經過三個步驟。

  • 分配內存
  • 初始化構造器
  • 將對象指向分配的內存地址

這樣的順序在雙重加鎖模式下是么有問題的,對象在初始化完成之后再把內存地址指向對象。

問題的根源

但是現代的JVM為了追求執行效率會針對字節碼(編譯器級別)以及指令和內存系統重排序(處理器重排序)進行調優,這樣的話就有可能(注意是有可能)導致2和3的順序是相反的,一旦出現這樣的情況問題就來了。

java源代碼到最終實際執行的指令序列:
java源代碼到最終實際執行的指令序列

前面的雙重檢查鎖定示例代碼的(instance = new Singleton();)創建一個對象。這一行代碼可以分解為如下的三行偽代碼:

memory = allocate();   //1:分配對象的內存空間
ctorInstance(memory);  //2:初始化對象
instance = memory;     //3:設置instance指向剛分配的內存地址

上面三行偽代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的,詳情見參考文獻1的“Out-of-order writes”部分)。2和3之間重排序之后的執行時序如下:

memory = allocate();   //1:分配對象的內存空間
instance = memory;     //3:設置instance指向剛分配的內存地址
                       //注意,此時對象還沒有被初始化!
ctorInstance(memory);  //2:初始化對象

多線程並發執行的時候的情況:

解決方案

基於Volatile的解決方案

先來說說Volatile這個關鍵字的含義:

  • 可以很好地解決可見性問題
  • 但不能確保原子性問題(通過 synchronized 進行解決)
  • 禁止指令的重排序(單例主要用到此JVM規范)

Volatile 雙重檢查加鎖模式

public class Singleton{
  private volatile static Singleton singleton;
  private Singleton(){
  }

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

基於類初始化的解決方案

利用靜態內部類的方式來創建,因為靜態屬性由JVM確保第一次初始化時創建,因此也不用擔心並發的問題出現。當初始化進行到一半的時候,別的線程是無法使用的,因為JVM會幫我們強行同步這個過程。另外由於靜態變量只初始化一次,所以singleton仍然是單例的。

這個方案的實質是:允許“問題的根源”的三行偽代碼中的2和3重排序,但不允許非構造線程(這里指線程B)“看到”這個重排序。

靜態內部類的方式

public class Singleton{

  private Singleton(){}

  public static Singleton getInstance(){
    return InnerClassSingleton.singleton;
  }

  private class InnerClassSingleton{
    protected static Singleton singleton = new Singleton();
  }
}

然而,雖然靜態內部類模式可以很好地避免並發創建出多個實例的問題,但這種方式仍然有其存在的隱患。

存在的隱患

  • 一旦一個實例被持久化后重新生成的實例仍然有可能是不唯一的。
  • 由於java提供了反射機制,通過反射機制仍然有可能生成多個實例。

序列化和反序列化帶來的問題:反序列化后兩個實例不一致了。

private static void singleSerializable() {
    try (FileOutputStream fileOutputStream=new FileOutputStream(new File("myObjectFilee.txt"));
         ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);) {
//            SingletonObject singletonObject = SingletonObject.getInstance();
//            InnerClassSingleton singletonObject = InnerClassSingleton.getInstance();
        EnumSingleton singletonObject = EnumSingleton.INSTANCE;
        objectOutputStream.writeObject(singletonObject);
        objectOutputStream.close();
        fileOutputStream.close();
        System.out.println(singletonObject.hashCode());
    } catch (IOException e) {
        e.printStackTrace();
    }

    try (FileInputStream fileInputStream=new FileInputStream(new File("myObjectFilee.txt"));
         ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);) {

//            SingletonObject singleTest=(SingletonObject) objectInputStream.readObject();
//            InnerClassSingleton singleTest=(InnerClassSingleton) objectInputStream.readObject();
        EnumSingleton singleTest=(EnumSingleton) objectInputStream.readObject();
        objectInputStream.close();
        fileInputStream.close();
        System.out.println(singleTest.hashCode());
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

問題點及解決辦法
ObjectInputStream中的readOrdinaryObject

if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
{
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        handles.setObject(passHandle, obj = rep);
    }
}

調用自定義的readResolve方法

protected Object readResolve(){
    System.out.println("調用了readResolve方法!");
    return  InnerClassSingleton.getInstance();
}

通過反射機制獲取到兩個不同的實例

private static void attack() {
    try {
        Class<?> classType = InnerClassSingleton.class;
        Constructor<?> constructor = classType.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        InnerClassSingleton singleton = (InnerClassSingleton) constructor.newInstance();
        InnerClassSingleton singleton2 = InnerClassSingleton.getInstance();
        System.out.println(singleton == singleton2);  //false
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}

解決方案 : 私有構造方法中進行添加標志判斷。

private InnerClassSingleton() {
    synchronized (InnerClassSingleton.class) {
        if (false == flag) {
            flag = !flag;
        } else {
            throw new RuntimeException("單例模式正在被攻擊");
        }
    }
}

單例最優方案,枚舉的方式

枚舉實現單例的優勢

  • 自由序列化;
  • 保證只有一個實例(即使使用反射機制也無法多次實例化一個枚舉量);
  • 線程安全;
public enum Singleton {
    INSTANCE;

    private Singleton(){}
}

Hibernate的解決方案

通過ThreadLocal的方式

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
public class HibernateSessionFactory {
    private static String CONFIG_FILE_LOCATION = "/hibernate.cfg.xml";
    private static final ThreadLocal threadLocal = new ThreadLocal();
    private static Configuration configuration = new Configuration();
    private static org.hibernate.SessionFactory sessionFactory;
    private static String configFile = CONFIG_FILE_LOCATION;

    static {
       try {
           configuration.configure(configFile);
           sessionFactory = configuration.buildSessionFactory();
       } catch (Exception e) {
           System.err.println("%%%% Error Creating SessionFactory %%%%");
           e.printStackTrace();
       }
    }

    private HibernateSessionFactory() {
    }

    public static Session getSession() throws HibernateException {
       Session session = (Session) threadLocal.get();
       if (session == null || !session.isOpen()) {
           if (sessionFactory == null) {
              rebuildSessionFactory();
           }
           session = (sessionFactory != null) ? essionFactory.openSession() : null;
           threadLocal.set(session);
       }
       return session;
    }
// Other methods...
}

參考文檔:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM