重走JAVA之路(二):面試中的單例模式(從入門到放棄)

散人丶發表於2019-03-20

前言

說到單例設計模式,大家應該都比較熟悉,也能說個一二三,單例單例,無非就是 保證一個類只有一個物件例項嘛,一般就是私有化建構函式,然後再暴露一個方法提供一個例項,確實沒錯,但是怎麼樣保證一個單例的安全性呢,私有建構函式,那如果反射強勢呼叫呢?再比如序列化一個物件後,反序列化呢?生成的物件是否還是一樣的?

1.常見的單例模式

單例模式現在的寫法確實也是有蠻多種,總結一下,大概有如下幾種:

  • 懶漢式寫法
  • 餓漢式寫法
  • DCL寫法(雙重判斷)
  • 靜態內部類寫法
  • 列舉類寫法

那麼每種寫法到底有什麼區別呢?哪種才是最適合的,話不多說,直接擼程式碼~

1.1 懶漢式寫法

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Version V2.0 <描述當前版本功能>
 */
public class SingleInstanceDemo {
    private static SingleInstanceDemo sSingleInstanceDemo;
    private SingleInstanceDemo(){

    }
    public synchronized static SingleInstanceDemo getInstance(){
        if (sSingleInstanceDemo==null)
            sSingleInstanceDemo = new SingleInstanceDemo();
        return sSingleInstanceDemo;
    }
}
複製程式碼

程式碼很簡單,這種方式是執行緒安全的,但是很明顯,每次呼叫方法,都需要先獲得同步鎖,效能比較低,不建議這麼寫

1.2 餓漢式寫法

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Date: 2019/2/21
 * @Version V2.0 <描述當前版本功能>
 */
public class SingleInstanceDemo {
    private static SingleInstanceDemo sSingleInstanceDemo = new SingleInstanceDemo();
    private SingleInstanceDemo(){

    }
    public synchronized static SingleInstanceDemo getInstance(){
        return sSingleInstanceDemo;
    }
}
複製程式碼

這種寫法,不能確保你的例項是在呼叫getInstance方法時生成的,因為類的載入機制是在可能需要使用到這個類的時候就載入(比如其他地方引用到了這個類名等等),不清楚的可以看下上篇文章 靜態變數的生命週期,所以這種也不能達到懶載入的效果。

1.3 DCL寫法

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Date: 2019/2/21
 * @Version V2.0 <描述當前版本功能>
 */
public class SingleInstanceDemo {
    private static SingleInstanceDemo sSingleInstanceDemo;
    private SingleInstanceDemo(){

    }
    public  static SingleInstanceDemo getInstance(){
        if (sSingleInstanceDemo==null){
            synchronized (SingleInstanceDemo.class){
                if (sSingleInstanceDemo==null){
                    sSingleInstanceDemo = new SingleInstanceDemo();
                }
            }
        }
        return sSingleInstanceDemo;
    }
}
複製程式碼

可以看到,把synchronized關鍵字是移到了內部,保證不用每次呼叫方法都得獲取同步鎖,效能有一定的提升,但是有一個問題,在Java指令中,物件的建立和賦值不是一步操作的,JVM會對程式碼進行一定的指令重排序(具體規則就不多介紹了,自行google),也就是說可能JVM會先直接賦值給instance成員,然後再去初始化這個sSingleInstanceDemo例項,這樣就會出現問題

當然也就解決辦法,加上volatile關鍵字就好了,可以禁止指令重排序

1.4 靜態內部類寫法

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Date: 2019/2/21
 * @Version V2.0 <描述當前版本功能>
 */
public class SingleInstanceDemo {
    public static class InnerClass{
        private static final SingleInstanceDemo sSingleInstanceDemo = new SingleInstanceDemo();
    }
    private SingleInstanceDemo(){

    }
    public  static SingleInstanceDemo getInstance(){
        return InnerClass.sSingleInstanceDemo;
    }
}
複製程式碼

乍一看!咦,好像和餓漢式有點像,只不過這裡宣告瞭一個私有的靜態內部類,這樣的區別就在於:

靜態sSingleInstanceDemo物件的生成一定是在呼叫getInstance()方法的時候生成的,因為它是跟隨著InnerClass這個類的載入而產生的,它本身是一個私有類,也保證了不會有其他的地方來呼叫InnerClass,這種寫法比較推薦

1.5 列舉類寫法

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Date: 2019/2/21
 * @Version V2.0 <描述當前版本功能>
 */
public enum SingleInstanceDemo {
    INSTANCE;

    private SingleInstanceDemo() {
    }
}
複製程式碼

單例的列舉實現在《Effective Java》中有提到,因為其功能完整、使用簡潔、無償地提供了序列化機制、在面對複雜的序列化或者反射攻擊時仍然可以絕對防止多次例項化等優點,單元素的列舉型別被認為是實現Singleton的最佳方法。

但是列舉類就記憶體消耗是比正常類要大的,所以,看情況選擇適合自己的最好

2 防止反射和反序列化

我們先來寫個demo來看看,是不是反射和反序列化真的會導致單例模式的問題

package com.example.hik.lib;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;

public class MyClass {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //我們通過靜態內部類方式,獲取單例物件
        SingleInstanceDemo instance = SingleInstanceDemo.getInstance();
        //通過反射來獲取一個物件
        SingleInstanceDemo instance2 = null;
        Class<SingleInstanceDemo> singleInstanceDemoClass = SingleInstanceDemo.class;
        try {
            Constructor<SingleInstanceDemo> constructor = singleInstanceDemoClass.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            instance2 = constructor.newInstance();
        } catch (Exception mE) {
            mE.printStackTrace();
        }
        System.out.println("reflect obj :"+(instance==instance2));
        // 1. 把物件instance寫入硬碟檔案
        FileOutputStream fos = new FileOutputStream("object.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(instance);
        oos.close();
        fos.close();
        // 2. 把硬碟檔案上的物件讀出來
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        SingleInstanceDemo instance3 = (SingleInstanceDemo) ois.readObject();
        System.out.println("Deserialize obj :"+(instance==instance3));
    }
}
複製程式碼

run一下上面的程式碼可以看到

reflect obj :false
Deserialize obj :false
Process finished with exit code 0

複製程式碼

居然都是false,也就是我們通過反射和反序列生成的物件和單例物件是不一樣的,那麼豈不是單例就不是單例的意義了,我們來改進一下程式碼

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Date: 2019/2/21
 * @Version V2.0 <描述當前版本功能>
 */
public class   SingleInstanceDemo implements Serializable {
    public static class InnerClass{
        public static final SingleInstanceDemo sSingleInstanceDemo = new SingleInstanceDemo();
    }
    private SingleInstanceDemo(){
        if (null!=InnerClass.sSingleInstanceDemo){
            throw new RuntimeException("不要用反射哦");
        }
    }
    public  static SingleInstanceDemo getInstance(){
        return InnerClass.sSingleInstanceDemo;
    }
    private Object readResolve() throws ObjectStreamException {
        return InnerClass.sSingleInstanceDemo;
    }
}
複製程式碼

解決辦法:

  • 序列化單例,重寫readResolve()方法
  • 在私有構造器裡判斷intance,如存在則拋異常(防止反射侵犯私有構造器)

再Run一下主程式碼,可以看到

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.example.hik.lib.MyClass.main(MyClass.java:20)
Caused by: java.lang.RuntimeException: 不要用反射哦
	at com.example.hik.lib.SingleInstanceDemo.<init>(SingleInstanceDemo.java:19)
	... 5 more
Deserialize obj :true
Process finished with exit code 0
複製程式碼

反射會丟擲異常,而反序列化後物件也是和之前的單例是一樣的,這樣就大功告成了~

主要還是希望小夥伴能真正弄清楚每個單例模式的意義和不足之處,這樣不管是在面試還是在日常開發中能夠更好的掌握單例模式~比心❤

相關文章