讀懂設計模式之單例模式

本文參考了以下博客

https://www.cnblogs.com/xiaobai1226/p/8487696.html
https://blog.csdn.net/qq_35860138/article/details/86477538
https://blog.csdn.net/li295214001/article/details/48135939/

單例模式是一種對象創建型模式,使用單例模式,可以保證爲一個類只生成唯一的一個實例對象。也就是說,在整個程序空間中,該類只存在一個實例對象。
GOF對單例模式的定義是:保證一個類,只有一個實例存在,同時提供能對該實例加以訪問的全局訪問方法。

單例具有以下基本特點

  • 聲明靜態私有類變量,且立即實例化,保證實例化一次
  • 私有構造,防止外部實例化
  • 提供public的getInstance()方法供外部獲取單例實例

1、懶漢式

特點:懶加載,在實例被調用時才初始化。但是線程不安全。

public class LazyInstance {
    private static LazyInstance instance = null;

    /**
     * 私有化構造函數
     */
    private LazyInstance() {
    }

    /**
     * 提供一個全局的靜態方法
     */
    public static LazyInstance getInstance() {
        if (instance == null) {
            instance = new LazyInstance();
        }
        return instance;
    }
}

2、加鎖懶漢式

由於線程不安全,我們可以通過synchronized關鍵字進行處理。此時代碼如下

public class LazySynchronized {

    private static LazySynchronized instance = null;

    /**
     * 私有化構造函數
     */
    private LazySynchronized() {
    }

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

這種實現方式雖然線程安全,但是每次獲取實例都要加鎖,耗費資源,其實只要實例已經生成,以後獲取就不需要再鎖了。

基於這些缺點,我們可以在使用雙重檢查,對懶漢式進行進一步升級

3、雙重檢查

public class LazyDoubleCheck implements Serializable {

    private static LazyDoubleCheck instance = null;

    /**
     * 私有化構造函數
     */
    private LazyDoubleCheck() {
    }

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

這樣寫,只把新建實例的代碼放到同步鎖中,爲了保證線程安全再在同步鎖中加一個判斷,
雖然看起來更繁瑣,但是同步中的內容只會執行一次,執行過後,以後經過外層的if判斷後,都不會在執行了,
所以不會再有阻塞。程序運行的效率也會更加的高。

這種方式看似很好的解決了同步的問題,但是其中還是有個坑。當多個線程訪問這個方法時,
可能會返回還未完成初始化的對象!

問題就在於instance = new LazyDoubleCheck();
根據Java類的初始化過程,步驟instance = new LazyDoubleCheck();並不是原子性的。
其中大概可以分爲三個步驟

    1. 分配內存給對象
    1. 初始化對象
    1. 設置instance指向剛分配的內存地址

但是在實際執行中代碼可能會被重排序,如下所示

    1. 分配內存給對象
    1. 設置instance指向剛分配的內存地址
    1. 初始化對象

在Java語言規範中,所有線程在執行Java程序時,必須要遵守intra-thread semantics規定。
intra-thread semantics保證重排序不會改變單線程內的程序執行結果。會允許那些在單線程內,不會改變單線程程序執行結果的重排序。例如上面的2和3步驟雖然被重排序了,但並不會影響程序執行結果。這個重排序在沒有改變單線程程序的執行結果的前提下,可以提高程序的執行性能。

爲了更好的理解這個問題,參考一下圖示

在這裏插入圖片描述

線程1執行到instance = new LazyDoubleCheck()時發生重排序先執行第三步,此時instance被賦值但是還未初始化對象,這時線程2訪問到第一個if中,由於此時判斷instance不爲null就直接返回此對象,線程2獲得的就是還未初始化完全的對象。注意:由於重排序問題並不一定會發生

針對上面出現的問題,我們可以有兩個解決方案

  • 1、不允許步驟2和3重排序
  • 2、允許重排序,但是不允許其他線程"看到"重排序過程

針對方案一有以下實現,基於volatile關鍵字禁止重排序

4、基於volatile的雙重檢查

public class LazyDoubleCheck implements Serializable {

    private static volatile LazyDoubleCheck instance = null;

    /**
     * 私有化構造函數
     */
    private LazyDoubleCheck() {
    }

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

volatile關鍵字具有內存可見性,關於這個以後會寫博客細談

針對方案二,我們通過靜態內部類的方式來實現。

5、靜態內部類

public class StaticInnerClass {

    private static class InstanceHolder {
        private static final StaticInnerClass instance = new StaticInnerClass();
    }

    /**
     * 私有化構造函數
     */
    private StaticInnerClass() {
    }

    public static StaticInnerClass getInstance() {
        return InstanceHolder.instance;
    }
}

這種實現方式是基於類的初始化鎖來實現類的懶加載安全性

利用了ClassLoader的機制來保證初始化instance時只有一個線程,同時實現了延時加載

優點:既避免了同步帶來的性能損耗,又能夠延遲加載

關於雙重檢查鎖的原理還可以參考這篇博客https://blog.csdn.net/li295214001/article/details/48135939/

6、餓漢式

特點:在類加載時就完成了初始化,所以類加載比較慢,但獲取對象的速度快,同時無法做到延時加載

public class Hungry {
     private static final Hungry hungry = new Hungry();
    
    /**
     * 構造函數私有化
     */
    private Hungry() {
    }
    
    public static Hungry getInstance() {
        return hungry;
    }
}

好處:線程安全;獲取實例速度快 缺點:類加載即初始化實例,內存浪費。如果實例未使用,仍然會生成佔用內存

以上方式都各自實現了單例模式,但是並不能完全保障單例安全。當我們對一個對象進行序列化與反序列化後,上述單實例就無法保證

public static void main(String[] args) throws Exception {
    Hungry s = Hungry.getInstance();
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
    oos.writeObject(s);
    oos.flush();
    oos.close();

    FileInputStream fis = new FileInputStream("singleton.obj");
    ObjectInputStream ois = new ObjectInputStream(fis);
    Hungry s1 = (Hungry) ois.readObject();
    ois.close();
    System.out.println(s + "\n" + s1);
}

當我們執行上述序列化代碼時,程序中就會生成兩個單例對象,針對這種方式我們要如何保證單例呢

7、序列化安全的單例

此處我們對餓漢式單例進行改造,使其保證序列化時仍然是單例。

解決方案:在單例中增加readResolve方法

public class Hungry  implements Serializable{
     private static final Hungry hungry = new Hungry();
    
    /**
     * 私有化構造函數
     */
    private Hungry() {
    }
    
    public static Hungry getInstance() {
        return hungry;
    }
    
    public Object readResolve(){
        return hungry;
    }
}

此時如果我們再測試序列化,就會發現返回的對象是同一個。這是爲什麼呢?爲什麼重寫readResolve()方法就能實現序列化安全呢。關鍵還是在於ObjectInputStreamreadObject()方法。查看源碼,我們就能一探究竟

以下JDK源碼基於JDK1.8.0_162

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }
        int outerHandle = passHandle;
        try {
            //返回的obj是readObject0()生成的
            Object obj = readObject0(false);
            handles.markDependency(outerHandle, passHandle);
            //中間省略部分代碼
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

查看方法可以看到返回的obj是readObject0()生成的,再進入readObject0()中

private Object readObject0(boolean unshared) throws IOException {
     	//省略部分代碼
        try {
            switch (tc) {
                case TC_ARRAY:
                    return checkResolve(readArray(unshared));
                case TC_ENUM:
                    return checkResolve(readEnum(unshared));
				//可以看出object類型返回的對象都是以下返回的
                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));
				//以下部分代碼省略
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

從以上部分代碼可以看出返回的代碼是checkResolve(readOrdinaryObject(unshared)),首先查看readOrdinaryObject(unshared)方法

private Object readOrdinaryObject(boolean unshared)    throws IOException{
        //省略部分代碼。。。
       //這個obj就是返回的對象,只需要關注這個對象就行了
        Object obj;
        try {
            //如果實現了serializable/externalizable接口isInstantiable()就會返回true
            //此時基於反射生成了實例
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }
        //中間省略部分代碼...
    	
    	//進入if條件中,
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod()) {
            //這裏通過反射會調用方法生成rep對象,在下面rep會賦值給obj對象
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                //此時obj指向剛纔通過invokeReadResolve()方法生成的對象
                handles.setObject(passHandle, obj = rep);
            }
        }
        return obj;
    }

readOrdinaryObject方法中待返回的對象obj首先指向通過反射生成的實例

obj = desc.isInstantiable() ? desc.newInstance() : null;

(因爲實現了serializable/externalizable接口isInstantiable()返回true)。

在下面的if條件中生成了rep對象,並且在下面將rep賦值給obj

handles.setObject(passHandle, obj = rep);

因此將重點放在Object rep = desc.invokeReadResolve(obj);中,查看invokeReadResolve方法

Object invokeReadResolve(Object obj)throws IOException, UnsupportedOperationException   {
        requireInitialized();
        if (readResolveMethod != null) {
            try {
                //就是一個反射調用,關鍵是這個readResolveMethod是什麼
                return readResolveMethod.invoke(obj, (Object[]) null);
            } catch (InvocationTargetException ex) {
               //省略部分代碼。。。
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

在類中查找readResolveMethod可以看到就是ObjectStreamClass中定義的一個私有變量

private Method readResolveMethod;

繼續查找可以看到賦值readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);

就是定義readResolve的Method對象,通過反射調用readResolve方法,將產生的對象在返回給obj再返回。

此時返回到readObject0方法中,readOrdinaryObject(unshared)返回的值傳到checkResolve()

private Object checkResolve(Object obj) throws IOException {
        if (!enableResolve || handles.lookupException(passHandle) != null) {
            return obj;
        }
    	//又調用了resolveObject方法,將返回的對象返回去
        Object rep = resolveObject(obj);
        if (rep != obj) {
            // The type of the original object has been filtered but resolveObject
            // may have replaced it;  filter the replacement's type
            if (rep != null) {
                if (rep.getClass().isArray()) {
                    filterCheck(rep.getClass(), Array.getLength(rep));
                } else {
                    filterCheck(rep.getClass(), -1);
                }
            }
            handles.setObject(passHandle, rep);
        }
        return rep;
    }

繼續查看resolveObject方法定義

protected Object resolveObject(Object obj) throws IOException {    
    return obj;
}

resolveObject中直接將obj返回了。此時所有流程基本走完。

到這裏我們就可以知道爲什麼在類中定義readResolve方法返回單例對象就可以防止序列化破壞單例了。

但是注意在上面代碼中有一步obj = desc.isInstantiable() ? desc.newInstance() : null;

雖然最後返回的是單例對象,但其實在執行過程還是通過反射生成了新的對象,雖然最後返回的並不是這個對象。

那麼如何防止反射生成多個對象呢?

8、防止反射的單例

對於餓漢模式來說,運行以下代碼通過反射創建對象仍然會產生多個實例

public static void main(String[] args) throws Exception {
        Class<Hungry> objClass = Hungry.class;
        Constructor<Hungry> constructor = objClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        Hungry instance = Hungry.getInstance();
        Hungry newInstance = constructor.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(newInstance == instance);
}

執行結果

Hungry@6d6f6e28
Hungry@135fbaa4
false

此時就需要對私有構造器改造一下,禁止其反射調用

private Hungry() {
    if (hungry != null) {       
        throw new RuntimeException("單例構造器禁止反射!");
    }
}

此時在執行上述代碼就會報錯

Exception in thread "main" java.lang.reflect.InvocationTargetException
	at ReflectTest.main(ReflectTest.java:24)
Caused by: java.lang.RuntimeException: 單例構造器禁止反射!
	at Hungry.<init>(Hungry.java:25)
	... 5 more

對於靜態內部類實現的單例模式也是這種方式防止反射

private StaticInnerClass() {
        if (InstanceHolder.instance != null) {
            throw new RuntimeException("單例構造器禁止反射!");
        }
    }

注意,對於這種調用getInstance方法時就已經完成類初始化的單例(餓漢模式和靜態內部類),這種方式可以防止反射。但是對於類似懶漢模式這種,調用getInstance纔會初始化的單例,可能會有一些問題。

注意在上面反射創建對象的代碼中,是先調用的getInstance方法,然後才使用反射創建了對象,這樣是沒有問題的。但如果兩者調換的位置,先通過反射創建對象,然後再調用getInstance方法。即便我們再構造器中添加代碼仍然會出現問題

 private LazyInstance() {
        if (instance != null) {
            throw new RuntimeException("單例構造器禁止反射!");
        }
 }
public static void main(String[] args) throws Exception {
        Class<LazyInstance> objClass = LazyInstance.class;
        Constructor<LazyInstance> constructor = objClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazyInstance newInstance = constructor.newInstance();
        LazyInstance instance = LazyInstance.getInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(newInstance == instance);
    }

運行結果

LazyInstance@6d6f6e28
LazyInstance@135fbaa4
false

當使用反射創建對象時,由於還沒調用getInstance方法,此時instance還爲null,就不會拋出異常。

當然我們可以設置一個變量flag標記是否初始化,然後再構造器中通過該變量進行判斷,但是既然構造器可以通過反射調用,設置變量仍然是可以被反射修改的

有沒有完美的單例模式嗎

9. 枚舉類型單例

這是單例模式的完美實現

public enum EnumSingleton {
    /**
     * 單實例
     */
    INSTANCE 

    public void doSomething() {
        System.out.println("you can do something");
    }

    public static void main(String[] args) {
        EnumSingleton.INSTANCE.doSomething();
    }

}

這種方式實現了線程安全,序列化安全,自帶防止反射

後記:理論上原型模式也會破壞單例,但是基本上沒人會在單例上運用原型模式,在下一篇原型模式之後再補充

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