設計模式之單例模式

0x01.定義與類型

  • 定義:保證一個類僅有一個實例,並提供一個全局訪問點
  • 類型:建立型
  • UML

singleton

  • 單例模式的基本要素html

    • 私有的構造方法
    • 指向本身實例的私有靜態引用
    • 以本身實例爲返回值的靜態的公有的方法

0x02.適用場景

  • 像確保任何狀況下都絕對只有一個實例
  • 須要頻繁實例化而後銷燬的對象。
  • 建立對象時耗時過多或者耗資源過多,但又常常用到的對象。
  • 有狀態的工具類對象。
  • 頻繁訪問數據庫或文件的對象。

0x03.單例模式的優缺點

1.優勢

  • 在內存裏只有一個實例,減小了內存開銷
  • 能夠避免對資源的多重佔用
  • 避免重複建立對象,提升性能
  • 設置全局訪問點,嚴格控制訪問

2.缺點

  • 沒有接口,擴展困難
  • 違反開閉原則

0x04.單例模式的幾種實現方式

1.餓漢式

  • 餓漢式:顧名思義,對象比較飢餓,因此一開始就建立好了。餓漢式也是單例模式的最簡單實現。
  • Java實現
/**
 * 餓漢式
 * 一開始就new好了
 */
public class HungrySingleton implements Serializable {

    /**
     * 能夠直接new也能夠適用靜態塊中建立
     * */
    private final static HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton1();
    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
    /**
     * 私有構造函數
     */
    private HungrySingleton() {}
}
  • 餓漢式的單例模式,對象一開始就建立好了。不須要考慮線程安全問題。
  • 餓漢式單例模式若是消耗資源比較多,而對象未被適用則會形成資源浪費。

2.懶漢式

  • 懶漢式:說明類對象比較懶,沒有直接建立,而是延遲加載的,是第一次獲取對象的時候才建立。懶漢式的單例模式應用較多。

a.第一個版本的Java實現(非線程安全)

/**
 * 懶漢式
 * 線程不安全
 */
public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    //線程不安全,當有兩個線程同時建立對象,會違背單例模式
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            //會發生指令重排
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
    private LazySingleton() {}
}
  • 這個版本的懶漢式會出現線程安全的問題,當兩個線程同時訪問getInstance()靜態方法時,lazySingleton還未建立,就會建立出兩個實例,違背了單例模式。
  • 這裏能夠在getInstance()方法添加同步鎖synchronized解決,也能夠在方法體添加類鎖,可是這樣至關於徹底鎖住了getInstance(),會出現性能問題。
  • 推薦適用下面這種方式

b.雙重檢查鎖double check懶漢式(線程安全,一般適用這種方式)

/**
 * 懶漢式
 * 線程不安全
 */
public class LazyDoubleCheckSingleton {

    //volatile 禁止指令重排序
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    /**
     * 在靜態方法中直接加synchronized至關於鎖了類
     * @return
     */
    public static LazyDoubleCheckSingleton getInstance() {
        //一樣實鎖類, 指令重排序
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    /**
                     * 1.分配內存給這個對象
                     * 2.初始化對象
                     * 3.設置lazyDoubleCheckSingleton指向剛分配的內存
                     * 2 3 順序有可能發生顛倒
                     * intra-thread semantics 不會改變單線程執行結果,指令重排序
                     */
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
    private LazyDoubleCheckSingleton() {}
}
  • 雙重檢查,只有對象爲空的時候纔會須要同步鎖,而第二次判斷是否爲null,是對象是否已經建立。
  • 添加volatile關鍵字,防止指令重排序。

c.基於靜態內部類的延遲加載方案

  • 私有靜態類的延遲加載
public class StaticInnerClassSingleton {

    /**
     * 看靜態類的初始化鎖那個線程能夠拿到
     */
    private static class InnerClass {
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }

    private StaticInnerClassSingleton () {
        if (InnerClass.staticInnerClassSingleton != null) {
            throw new RuntimeException("單例對象禁止反射調用");
        }
    }
}
  • 將延遲初始化交給靜態類的初始化

3.容器單例

  • 使用靜態容器方式來實現多單例類
public class ContainerSingleton {

    //靜態容器, 注意map不是線程安全的,若是爲了線程安全可使用HashTable或者ConcurrentHashMap
    private static Map<String, Object> singletonMap = new HashMap<>();

    public static void putInstance (String key, Object instance) {
        if (key != null && key.length() != 0) {
            if (!singletonMap.containsKey(key)) {
                singletonMap.put(key, instance);
            }
        }
    }

    public static Object getInstance (String key) {
        return singletonMap.get(key);
    }
}
  • 容器單例若是要保證線程安全性,建議使用ConcurrentHashMap
  • 一般使用容器單例狀況是:單例對象比較多,須要統一維護。

4.枚舉單例模式(推薦使用)

  • 枚舉單例是從JVM層面上作的限制
public enum EnumInstance {

    /**
     * 具體的單例實例
     */
    INSTANCE {
        protected void  printTest () {
            System.out.println("K.O print Test!");
        }
    };
    private Object data;
    protected abstract void printTest();
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}
  • 後續會介紹到,單例模式完美防護了反射與序列化攻擊

5.ThreadLocal線程單例(並非嚴格意義上的單例模式)

  • 有一部分場景,要求對象的生命週期隨着線程
/**
 * 線程級單例模式
 */
public class ThreadLocalInstance {

    //靜態的ThreadLocal類保存對象
    private static final ThreadLocal<ThreadLocalInstance> threadLocal =
            ThreadLocal.withInitial(ThreadLocalInstance::new);

    private ThreadLocalInstance () {}

    public static ThreadLocalInstance getInstance () {
        return threadLocal.get();
    }
}
  • 經過getInstance()獲取該線程的實例。

0x05.單例模式的序列化與反射攻擊

1.序列化攻擊

  • 之前面餓漢式舉例
  • 測試代碼
public class SerializableTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //1.實例化
        HungrySingleton instance = HungrySingleton.getInstance();

        //2.寫入本地文件
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        //3.讀取
        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton) ois.readObject();

        //4.比較
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • 輸出結果
org.ko.singleton.hungry.HungrySingleton@135fbaa4
org.ko.singleton.hungry.HungrySingleton@568db2f2
false
  • 解決方案:添加readResolve()方法
  • 修改後
/**
 * 餓漢式
 * 一開始就new好了
 */
public class HungrySingleton implements Serializable {

    private final static HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

    /**
     * 寫完後,序列化對象會經過反射調用這個方法
     * 徹底是ObjectInputStream寫死的,並無任何繼承關係
     * 其實每次序列化 反序列化 都已經建立對象了,只是最後返回的這一個
     * @return
     */
    private Object readResolve () {
        return hungrySingleton;
    }

    private HungrySingleton() {}
}
  • 輸出結果
org.ko.singleton.hungry.HungrySingleton@135fbaa4
org.ko.singleton.hungry.HungrySingleton@135fbaa4
true
  • 爲何添加了readResolve()方法就能夠了?java

    • ObjectInputStream源碼中,讀取文件時寫死判斷是否有readResolve()方法,有調用這個方法,沒有則從新建立對象。

2.反射攻擊

  • 經過反射攻擊,實例化對象建立出第二個單例對象
/**
 * 類加載時就已經建立好對象
 */
public class ReflectTest {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = HungrySingleton.class;

        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);

        //反射建立
        HungrySingleton instance = HungrySingleton.getInstance();

        //正常建立
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);

        //StaticInnerClassSingleton類也是同樣的
    }
}
  • 測試結果
org.ko.singleton.hungry.HungrySingleton@1540e19d
org.ko.singleton.hungry.HungrySingleton@677327b6
false
  • 解決辦法:在構造方法拋出異常
/**
 * 餓漢式
 * 一開始就new好了
 */
public class HungrySingleton implements Serializable {

    private final static HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

    /**
     * 寫完後,序列化對象會經過反射調用這個方法
     * 徹底是ObjectInputStream寫死的,並無任何繼承關係
     * 其實每次序列化 反序列化 都已經建立對象了,只是最後返回的這一個
     * @return
     */
    private Object readResolve () {
        return hungrySingleton;
    }

    private HungrySingleton() {
        /**
         * 對一開始就建立好了的類有效
         */
        if (hungrySingleton != null) {
            throw new RuntimeException("單例對象禁止反射調用");
        }
    }
}
  • 再次測試輸出結果
Exception in thread "main" 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 org.ko.singleton.ReflectTest1.main(ReflectTest1.java:23)
Caused by: java.lang.RuntimeException: 單例對象禁止反射調用
  at org.ko.singleton.hungry.HungrySingleton2.<init>(HungrySingleton2.java:36)
  ... 5 more
  • 注意使用這種方式防止反射攻擊,餓漢式正常,懶漢式由於建立對象的時機不一樣仍是會出現問題,這種方式只能作到儘可能的防護。

3.關於枚舉單例模式防止序列化與反射

  • 枚舉模式的實例自然具備線程安全性,防止序列化與反射的特性
  • 驗證代碼
/**
 * 枚舉類測試
 */
public class SerializableTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //測試枚舉類型
        EnumInstance instance = EnumInstance.getInstance();
        //設置對象
        instance.setData(new Object());

        //寫入文件
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        //讀取文件
        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        EnumInstance newInstance = (EnumInstance) ois.readObject();

        //比較實例
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);

        //比較實例中引用對象
        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
        System.out.println(instance.getData() == newInstance.getData());
    }
}
  • 測試結果:
INSTANCE
INSTANCE
true
java.lang.Object@5fd0d5ae
java.lang.Object@5fd0d5ae
true
  • 反射攻擊測試
/**
 * 類加載時就已經建立好對象
 */
public class ReflectTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = EnumInstance.class;

        Constructor constructor = objectClass.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);

        //反射對象
        EnumInstance newInstance = (EnumInstance) constructor.newInstance("K.O", 1);
        //實例對象
        EnumInstance instance = EnumInstance.getInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • 測試結果,枚舉類沒辦法經過構造函數建立實例
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
  at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
  at org.ko.singleton.ReflectTest3.main(ReflectTest3.java:21)
  • 枚舉類反編譯結果
//final的
public final class EnumInstance extends Enum{
    public static EnumInstance[] values(){
        return (EnumInstance[])$VALUES.clone();
    }

    public static EnumInstance valueOf(String name){
        return (EnumInstance)Enum.valueOf(org/ko/singleton/byenum/EnumInstance, name);
    }

    //私有構造器
    private EnumInstance(String s, int i){
        super(s, i);
    }

    public Object getData(){
        return data;
    }

    public void setData(Object data){
        this.data = data;
    }

    public static EnumInstance getInstance(){
        return INSTANCE;
    }

    //static final
    public static final EnumInstance INSTANCE;
    private Object data;
    private static final EnumInstance $VALUES[];

    //經過靜態塊加載它,比較像餓漢模式
    static {
        INSTANCE = new EnumInstance("INSTANCE", 0);
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }
}
  • 結論:若是不是特別重的對象,建議使用枚舉單例模式,它是JVM自然的單例。

0x06.單例模式關注的重點

  1. 私有構造器
  2. 線程安全
  3. 延遲加載
  4. 序列化和反序列化安全
  5. 反射攻擊安全

0x07.相關設計模式

  • 單例模式和工廠模式:工廠類能夠設計成單例模式。
  • 單例模式和享元模式:能夠經過享元模式來獲取單例對象

0x08.相關代碼

單例模式https://github.com/sigmako/design-pattern/tree/master/singletongit

0x09.參考文章

相關文章
相關標籤/搜索