最簡單又最複雜的單例模式

定義

保證一個類僅有一個實例,並提供一個全局訪問點

類型

創建型

適用場景

想確保任何情況下都絕對只有一個實例

優點

  • 在內存裏只有一個實例,減少了內存開銷
  • 可以避免對資源的多重佔用
  • 設置了全局訪問點,嚴格控制訪問

缺點

沒有接口,擴展困難

重點

私有構造器

這個是爲了禁止從單例外部調用構造函數來創建這個對象,爲了達到這個目的,必須設置構造函數的權限爲private

線程安全

線程安全在單例模式中是非常重要的

延遲加載

我們想使用的時候再創建

序列化和反序列化安全

一旦對單例進行序列化和反序列化的話,就會對單例進行破壞

反射

單例模式也要防止反射攻擊,雖然在平常寫代碼的時候並不會刻意這麼做,但是基於工程師的思想我們也要考慮下這個點

codnig

懶漢式

非線程安全的

/**
 *  懶漢式:
 */
public class LazySingleton {
    /*
     * 可以說比較懶,在初始化的時候是沒有創建的
     */
    private static LazySingleton lazySingleton=null;

    /**
     * 構造器是private的,爲了在外部不讓調用
     */
    private LazySingleton(){
    }

    /**
     * 線程不安全的,
     * @return
     */
    public static LazySingleton getInstance(){
        if(lazySingleton==null){
            lazySingleton=new LazySingleton();
        }
        return lazySingleton;
    }

}

懶漢式重在懶,在加載的時候沒有初始化。但是對於線程安全是沒有考慮的,比如兩個線程都到斷點處
在這裏插入圖片描述
線程1到24行後沒有執行呢,線程2在23行執行時候判斷到是true,也進入24行,這樣LazySingleton就被初始化了兩次。我們可以通過多線程debug的方式讓這種現象出現,具體操作是在Idea中右鍵斷點,彈出對話框
在這裏插入圖片描述
默認是選中All的,也就是主線程中,我們可以選擇thread,即爲線程模式,並且右邊還有個MakeDefault的按鈕,這個可以設置下次設置斷點時候默認選擇的模式

線程安全的

有兩種改進懶漢式非線程安全的方式

第一種:加synchronized關鍵字
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton==null){
            lazySingleton=new LazySingleton();
        }
        return lazySingleton;
    }

如果在靜態方法上加synchronized 關鍵字,那麼鎖的是這個類的class文件,不是靜態方法,相當於鎖的是在堆內存中生成的對象。還有一種寫法,和上面的是一樣的

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

我們知道同步鎖是比較消耗資源,而且synchronized修飾static方法的時候,鎖的是這個class,鎖的範圍是非常大的,對性能會有一定影響。

Double-check雙重檢查方式(有隱患)

這種方式兼顧了性能和線程安全,而且也是懶加載的

/**
 * 懶加載雙重檢查
 */
public class LazyDoubleCheckSingleton {

    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    private LazyDoubleCheckSingleton() {
    }

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

我們在if裏面鎖住類,那麼就代表if還是可以進來的,需要在進來後的創建對象代碼上加鎖,這樣比起第一種情況,鎖的範圍就縮小了,性能也影響變小了,同時也保證了線程安全。

但是上面的代碼還有隱患,出在if (lazyDoubleCheckSingleton == null) {lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();,爲什麼呢?
首先在if (lazyDoubleCheckSingleton == null) {行時候,雖然判斷了是否爲空,但是如果不爲空,但是對象又沒有完成初始化,也就是lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();行沒有執行結束,這是什麼意思呢?
再來看下new的這行代碼,看起來是一行,實際上經歷了3個步驟

  1. 分配內存給這個對象
  2. 初始化這個對象
  3. 設置lazyDoubleCheckSingleton指向剛分配的內存地址

也就是說new這行代碼實際上執行了3行操作,在2、3步驟時候有可能出現重排序,順序顛倒變成

  1. 分配內存給這個對象
  2. 設置lazyDoubleCheckSingleton指向剛分配的內存地址
  3. 初始化這個對象

這就要說下java規範裏的,有說必須要遵守intra-thread semantics(線程內語義)這樣的一個規定,它保證重排序不會改變單線程內的程序執行結果,比如1、2、3互換位置,不會改變單線程的執行結果,也就是說上面的2、3互換是允許的。
在這裏插入圖片描述
上圖可以看出,2、3怎麼換順序,4的訪問結果都是一樣的,所以2、3的重排序對結果並沒有影響,當然這個重排序並不是100%命中的,是有一定概率的,但是這種隱患我們也要消除,左邊是單線程沒有什麼問題。我們看右邊線程1,假設線程0重排序了,走了3,這時候線程1從上至下開始判斷instance是否爲null,這個時候判斷出來了,instance並不爲null,因爲它有指向內存空間,然後線程1開始訪問對象,也就是說線程1比線程0更早的訪問對象,所以線程1訪問到的對象是在線程0中還沒有初始化完成的對象,這個對象並沒有被完整的初始化上,系統就要報異常了。
那我們怎麼辦呢?可以有兩種方法,可以阻止重排序,或者允許線程0重排序但線程1不能看到這個重排序

Double-check雙重檢查方式(volatile禁止重排序)
public class LazyDoubleCheckSingleton {
	//只需要一個小小的改動,就可以實現線程安全的延遲初始化
    private volatile static  LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    private LazyDoubleCheckSingleton() {
    }

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

這樣重排序就會被禁止,在多線程的時候cpu也有共享內存,我們在加了volatile關鍵字之後,所有的線程就都能看到共享內存的最新狀態,保證了內存的可見性,這裏就和共享內存有關了,在加了volatie關鍵字後,在共享變量進行寫操作的時候,會多出一些彙編代碼,起到兩個作用,第一是將當前當前處理器緩存的數據寫到系統內存,這個寫會內存的操作會使在其它cpu裏緩存了該內存地址的數據無效,因爲其它cpu緩存的數據無效了,所以它們又從共享內存同步數據,這樣就保證了內存的可見性。這裏面是使用的緩存一致性協議,當處理器發現我這個緩存已經無效了,所以我在進行操作的時候會重新從系統內存中把數據讀到處理器的緩存裏。

靜態內部類延遲加載(基於類初始化)

這種方式是讓線程1看不到線程0的重排序,是通過靜態內部類來解決

/**
 *  靜態內部類延遲加載單例
 */
public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton=new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
     private StaticInnerClassSingleton(){
        
    }
}

我們看下原理
在這裏插入圖片描述
jvm在類的初始化階段,也就是class被加載後,並且被線程使用之前,都是類的初始化階段。在這個階段,會執行類的初始化,那在執行類的初始化期間,jvm會去獲取一個鎖,這個鎖可以同步多個線程對一個類的初始化,也就是上圖綠色的部分。基於這個特性我們可以實現基於靜態內部類的,並且是線程安全的,延遲初始化方案,看上圖,右邊框中的2、3指令重排序,對於線程1並不會看到,也就是說非構造線程,是不允許看到這個重排序的,因爲之前我們講的是線程0來構造這個單例對象,初始化一個類,包括執行這個類的靜態初始化,還有初始化在這個類中聲明的靜態變量,根據java語音規範主要分爲5種情況:

  1. 首次發生的時候,一個類將被立刻初始化,這個類所說的類泛指包括接口也是一個類。
  2. 包括類中聲明的一個靜態方法被調用
  3. 類中聲明的靜態成員被賦值
  4. 類中聲明的一個靜態成員被使用,並且這個靜態成員不是一個常量成員
  5. 類是頂級類,並且這個類中有斷言語句

只要發生上面說的5種情況,這個類都會被初始化。再看上圖,線程0、線程1同時獲得綠色框的鎖的時候,假設線程0獲取了鎖,線程0執行靜態內部類的初始化,對於靜態內部類的2、3存在重排序,但是線程1是無法看到這個重排序的,因爲這裏面有一個class對象的初始化鎖,因爲有鎖所以對於線程0而言,初始化這個靜態內部類的時候,把這個instance new出來,我們線程1看不到,因爲線程1在綠色區等待,所以靜態內部類是基於類初始化的延遲加載解決方案。

餓漢式

/**
 * 餓漢式
 */
public class HungrySingleton {
    private final static HungrySingleton hungrySingleton=new HungrySingleton();
    private HungrySingleton(){

    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    
}

優點是在類加載的時候就完成了初始化,避免了線程的同步問題。
缺點也是由於類在加載的時候就完成了初始化,沒有延遲加載的效果,如果這個類從始至終我們系統都沒用過,還會造成內存的浪費,我們也可以將對象的初始化放在靜態代碼塊裏。

/**
 * 餓漢式
 */
public class HungrySingleton {
    private final static HungrySingleton hungrySingleton ;
	static{
		hungrySingleton =new HungrySingleton();
	}
    private HungrySingleton(){

    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    
}

由於final修飾的變量必須在類加載時完成初始化,因此在懶漢式中靜態私有單例類型變量不能加final,惡漢式中就可加可不加了。

反序列化破壞單例模式及解決方案

我們以懶漢式爲例,進行演示

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance=HungrySingleton.getInstance();
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        //再讀取出來
        File file=new File("singleton");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        //和寫入時的對象進行對比,看是否是同一個對象
        HungrySingleton newInstace= (HungrySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstace);
        System.out.println(instance == newInstace);
    }
}

運行結果:

Exception in thread "main" java.io.NotSerializableException: com.design.pattern.creational.singleton.HungrySingleton
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at com.design.pattern.creational.singleton.Test.main(Test.java:25)

這是因爲HungrySingleton中沒有實現Serializable接口,實現後再運行

com.design.pattern.creational.singleton.HungrySingleton@7530d0a
com.design.pattern.creational.singleton.HungrySingleton@3d494fbf
false

可以發現結果是不相等的,這就違背了單例模式的初衷,通過序列化和反序列化得到了不同的對象,我們希望得到同一個對象,這個事情怎麼解決呢?
HungrySingleton中寫個方法

public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton=new HungrySingleton();
    private HungrySingleton(){
        System.out.println("HungrySingleton");
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }

    /**
     * 增加該方法,返回這個單例對象
     * @return
     */
    private Object readResolve(){
        return hungrySingleton;
    }

}

再運行下,看看結果

com.design.pattern.creational.singleton.HungrySingleton@7530d0a
com.design.pattern.creational.singleton.HungrySingleton@7530d0a
true

爲什麼加了readResolve方法就可以了呢?從HungrySingleton的父類裏找,根本就沒有找到該方法
在這裏插入圖片描述
說明它並不是Object這個對象的方法,方法名字爲什麼又叫readResolve呢?我們較個真,繼續看下ois.readObject(),在它裏面有一行if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()),判斷了如果反序列化的對象有readResolve方法,就反射調用該方法得到該方法的返回值,並替換掉反序列化時實例化的對象 在這裏插入圖片描述

反射攻擊和解決方案

我們寫下反射攻擊的代碼

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass=HungrySingleton.class;
        Constructor constructor=objectClass.getDeclaredConstructor();
        HungrySingleton instance=HungrySingleton.getInstance();
        HungrySingleton newInstance= (HungrySingleton) constructor.newInstance();

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

運行結果

Exception in thread "main" java.lang.IllegalAccessException: Class com.design.pattern.creational.singleton.Test can not access a member of class com.design.pattern.creational.singleton.HungrySingleton with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)

看到報錯,是說HungrySingleton 的構造方法是私有的,那我們改成可訪問的,加一行代碼constructor.setAccessible(true);,再運行後

com.design.pattern.creational.singleton.HungrySingleton@7440e464
com.design.pattern.creational.singleton.HungrySingleton@49476842
false

Process finished with exit code 0

可以看到反射和單例模式創建的對象是不一樣的,這就違背了單例模式的初衷,我們怎麼辦呢?
看下餓漢式有個特點,它是在類加載的時候就初始化了,那我們可以在構造器中進行判斷

 private HungrySingleton(){
       if(hungrySingleton!=null){
           throw new RuntimeException("單例構造器禁止反射調用");
       }
    }

再運行就會報異常,這樣對反射攻擊進行了保護,但是這種方式有個特點,對類加載時候就實例化是OK的,但是對於懶加載的單例就不是都ok了,例如先反射方式創建,再單例方式創建,就會創建兩個不同的對象,並且增加一個是否懶加載過的標識,反射都可以修改該標識,所以這種方式對於懶加載的單例是不能阻擋反射攻擊的

/**
 *  懶漢式:
 */
public class LazySingleton2 {
    /*
     * 可以說比較懶,在初始化的時候是沒有創建的
     */
    private static LazySingleton2 lazySingleton=null;
    private static boolean flag = true;
    private LazySingleton2(){
        if(flag){
            flag= false;
        }else {
            throw new RuntimeException("單例構造器禁止反射調用");
        }
    }

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

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass=LazySingleton2.class;
        Constructor c=objectClass.getDeclaredConstructor();
        c.setAccessible(true);

        LazySingleton2 o1=LazySingleton2.getInstance();
        LazySingleton2 o2= (LazySingleton2) c.newInstance();

        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1==o2);
    }

}

運行結果

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 com.design.pattern.creational.singleton.LazySingleton2.main(LazySingleton2.java:44)
Caused by: java.lang.RuntimeException: 單例構造器禁止反射調用
	at com.design.pattern.creational.singleton.LazySingleton2.<init>(LazySingleton2.java:19)
	... 5 more

看到我們加的flag是生效了,沒有問題,是好使的,阻止了反射攻擊。但是我們可以破壞它,怎麼做呢?

 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Class objectClass=LazySingleton2.class;
        Constructor c=objectClass.getDeclaredConstructor();
        c.setAccessible(true);

        LazySingleton2 o1=LazySingleton2.getInstance();
        //破壞阻止反射攻擊
        Field flag=o1.getClass().getDeclaredField("flag");
        flag.setAccessible(true);
        flag.set(o1,true);
        LazySingleton2 o2= (LazySingleton2) c.newInstance();

        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1==o2);
    }

運行結果

com.design.pattern.creational.singleton.LazySingleton2@49476842
com.design.pattern.creational.singleton.LazySingleton2@78308db1
false

可以看到反射攻擊成功,也就是說我們通過反射把flag改爲true了,無論裏面加多麼複雜的邏輯,都是沒有用的,可以通過反射進行任意修改。
到這裏,你可能認爲單例模式是最簡單的,看上去呢確實是最簡單的,但是說複雜也是最複雜的

枚舉單例

可保證防止反射攻擊,有可以保證不被序列化破壞。這也是Effective Java書裏推薦的單例模式。

/**
 * 枚舉類單例模式
 */
public enum  EnumInstance {
    INSTANCE;
    /**可以多個,這裏演示只寫一個data*/
    private Object data;

    public Object getData() {
        return data;
    }

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

    public static EnumInstance getInstance(){
        return INSTANCE;
    }
}

我們主要關注的是枚舉單例的序列化機制,和反射攻擊。
枚舉類天然的可序列化機制,可以保證不會出現多次實例化的情況,即使在複雜的序列化和反射攻擊情況下枚舉類型的單例模式都沒有問題,只不過現在的寫法可能看起來不太自然,但是枚舉類實現單例可能是實現單例中的最佳實踐,Effective java中也是強烈推薦。

序列化影響測試

我們測試下序列化,先測試枚舉持有的INSTANCE

/**
 * 應用類
 */
public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
		EnumInstance instance=EnumInstance.getInstance();
        //再讀取出來
        File file=new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        //和寫入時的對象進行對比,看是否是同一個對象
        EnumInstance newInstace= (EnumInstance) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstace);
        System.out.println(instance == newInstace);
    }
}

運行結果

INSTANCE
INSTANCE
true

結果已經出來了,輸出的時候是兩個INSTANCE,並且是相等的。我們主要測試的是枚舉類持有的data對象做實驗,我們繼續使用data做實驗

/**
 * 應用類
 */
public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
		EnumInstance instance=EnumInstance.getInstance();
        //測試枚舉類持有的data初始化好,再序列化,之後看看data是不是同一個
        instance.setData(new Object());		
        //再讀取出來
        File file=new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        //和寫入時的對象進行對比,看是否是同一個對象
        EnumInstance newInstace= (EnumInstance) ois.readObject();
        System.out.println(instance.getData());
        System.out.println(newInstace.getData());
        System.out.println(instance.getData() == newInstace.getData());
    }
}

運行結果

java.lang.Object@b1bc7ed
java.lang.Object@b1bc7ed
true

枚舉類就是這麼強大,那麼序列化和反序列化枚舉類是怎麼處理的呢?我們打開ObjectInputStream裏面有一個方法叫readEnum(boolean unshard)

private Enum<?> readEnum(boolean unshared) throws IOException {
        //各種校驗開始
        if (bin.readByte() != TC_ENUM) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        if (!desc.isEnum()) {
            throw new InvalidClassException("non-enum class: " + desc);
        }

        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(enumHandle, resolveEx);
        }
		//重點開始
		//獲取到枚舉對象的名name,
        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                //通過類型和名稱獲取枚舉常量,因爲name是惟一的,並且對應唯一一個枚舉常量
                //取到的肯定是唯一的常量對象,沒有創建新的對象,維持了對象的單例屬性
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
    }

可以看到,枚舉對於序列化的破壞是不受影響的

反射攻擊測試

/**
 * 應用類
 */
public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
		 //枚舉單例 反射攻擊測試
        Class objectClass=EnumInstance.class;
        Constructor constructor=objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumInstance instance=EnumInstance.getInstance();
        EnumInstance newInstance= (EnumInstance) constructor.newInstance();

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

運行結果

Exception in thread "main" java.lang.NoSuchMethodException: com.design.pattern.creational.singleton.EnumInstance.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.design.pattern.creational.singleton.Test.main(Test.java:69)

看到運行報異常了,報的是NoSuchMethodException,這個異常出現在這一行Constructor constructor=objectClass.getDeclaredConstructor();獲取構造器時沒有獲取無參構造器,這個是爲什麼呢?我們來看下枚舉類java.lang.Enum的源碼,只有117行有個有參構造方法

protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

我們改下反射攻擊的代碼

/**
 * 應用類
 */
public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
		 //枚舉單例 反射攻擊測試
        Class objectClass=EnumInstance.class;
        Constructor constructor=objectClass.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        EnumInstance instance=EnumInstance.getInstance();
        EnumInstance newInstance= (EnumInstance) constructor.newInstance("test",666);

        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 com.design.pattern.creational.singleton.Test.main(Test.java:72)

看到反射獲取構造器成功了,但是構造器在構造時候又報異常了,Cannot reflectively create enum objects不能反射創建枚舉類對象,在Constructor.java:417

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

看到通過反射攻擊失敗了

反編譯工具Jad

jad下載是一個超級有用的反編譯工具,我們在命令行中使用

>jad EnumInstance.class
Parsing EnumInstance.class... Generating EnumInstance.jad

我們打開反編譯的jad文件

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumInstance.java

package com.design.pattern.creational.singleton;

//類是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(com/design/pattern/creational/singleton/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
        });
    }
}

看到一個枚舉類其實是非常符合單例模式的要求的,final代表這個類不能夠被繼承,私有構造器不允許外部實例化,類變量時靜態的並且沒有延遲初始化,通過靜態代碼塊來初始化,同時也是線程安全的。除此之外,還有反序列化類、反射來爲枚舉類保駕護航,適當的拋出異常,所以枚舉類實現單例模式還是比較優雅的。

枚舉類中使用方法

/**
 * 枚舉類中使用方法
 */
public enum  EnumInstance {
    INSTANCE{
        @Override
        protected void printTest(){
            System.out.println("print test");
        }
    };
    /**聲明一個同名的抽象方法,不然沒法調用{}中聲明的方法*/
    protected abstract void printTest();
    private Object data;

    public Object getData() {
        return data;
    }

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

    public static EnumInstance getInstance(){
        return INSTANCE;
    }
}

測試代碼

  EnumInstance instance=EnumInstance.getInstance();
        instance.printTest();

運行結果

print test

反編譯後枚舉類的代碼

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumInstance.java

package com.design.pattern.creational.singleton;

import java.io.PrintStream;

public abstract class EnumInstance extends Enum
{

    public static EnumInstance[] values()
    {
        return (EnumInstance[])$VALUES.clone();
    }

    public static EnumInstance valueOf(String name)
    {
        return (EnumInstance)Enum.valueOf(com/design/pattern/creational/singleton/EnumInstance, name);
    }

    private EnumInstance(String s, int i)
    {
        super(s, i);
    }

    protected abstract void printTest();

    public Object getData()
    {
        return data;
    }

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

    public static EnumInstance getInstance()
    {
        return INSTANCE;
    }


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

    static 
    {
        INSTANCE = new EnumInstance("INSTANCE", 0) {

            protected void printTest()
            {
                System.out.println("print test");
            }

        }
;
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }
}

總結

單例模式實現的方案很多,每一種都有自己的優缺點,所以我們要根據不同的業務場景來選擇不同的單例模式的實現方案。還有這一種模式是如何演進的,這幾種單例方式都解決了什麼問題,又存在什麼問題,單例模式無論在校招還是社招,如果問設計模式的話99%都會問單例這個模式,單例模式看起來非常簡單,對於初學者來說也是很簡單的,但是就是隔着一層窗戶紙,只要一捅破小夥伴會發現單例模式是這些設計模式中最複雜的一個模式。如果要深入研究的話,這裏面的知識點還是非常有意思的。相信小夥伴們通過學習單例模式,對於提高自己的思維能力、編碼能力、實操能力,這些肯定會有所提高的。還有看到這裏了,希望小夥伴們在以後的面試中,千萬不要在單例模式上丟分,一定要在這裏加分

容器單例模式

public class ContainerSingleton {
    private static Map<String,Object> singletonMap=new HashMap<>();
    public static void putInstance(String key,Object instance){
        if(StringUtils.isNotBlank(key) && instance!=null){
            if(!singletonMap.containsKey(key)){
                singletonMap.put(key,instance);
            }
        }
    }

    public static Object getInstance(String key){
        return singletonMap.get(key);
    }
}

看到使用的是HashMap,本身不是線程安全的,會有隱患。我們如果改成HashTable會不會就變線程安全的,當然是線程安全的,但會影響性能,頻繁去取的時候都有同步鎖,會影響性能。我們可以折中使用ConcurrentHashMap,但是我們使用了靜態的ConcurrentHashMap,而且直接操作了這個Map,在這種場景下,ConcurrentHashMap並不是絕對的線程安全。
所以我們不考慮反射序列化這種機制的話,這種容器單例模式也是有一定的使用場景的,在安卓的SDK源碼中使用的也比較多,JDK中也有這種模式使用的影子。這個在使用過程中,不建議使用HashTable,這個HashMap也是做了一個平衡,如果一個業務中,單例對象特別多,我們就可以考慮使用一個容器把這些單例對象統一管理,優點就是統一管理節省資源相當於一個緩存,缺點線程不安全

ConcurrentHashMap不是絕對安全

轉載

public class ThreadSafeTest {
public static Map<Integer,Integer> map=new ConcurrentHashMap<>();
public static void main(String[] args) {
    ExecutorService pool1 = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 10; i++) {
        pool1.execute(new Runnable() {
            @Override
            public void run() {
                Random random=new Random();
                int randomNum=random.nextInt(10);
                if(map.containsKey(randomNum)){
                    map.put(randomNum,map.get(randomNum)+1);
                }else{
                    map.put(randomNum,1);
                }
            }
        });
    }
}
}

這段代碼是用10個線程測試10以內各個整型隨機數出現的次數,表面上看採用ConcurrentHashMap進行contain和put操作沒有任何問題。但是仔細想下,儘管 containsKey和 put 兩個方法都是原子的,但在jvm中並不是將這段代碼做爲單條指令來執行的,例如:假設連續生成2個隨機數1,map的 containsKey 和 put 方法由線程A和B 同時執行 ,那麼有可能會出現A線程還沒有把 1 put進去時,B線程已經在進行if 的條件判斷了,也就是如下的執行順序:

A:  map 正在放置隨機數 1 進去 
A 被掛起
B:  執行 map.containsKey(1) 返回false
B:  將隨機數 1 放進 map
A:  將隨機數 1 放進 map
map 中key 1 的value值 還是爲 1

這樣會導致雖然生成了2次隨機數 1 ,它的value值還是1,我們期望的結果應該是2,這並不是我們想要的結果。概括的說就是兩個線程同時競爭map, 但他們對map訪問順序必須是先 containsKey 然後再 put 對象進去,即產生了競態條件。解決方法當然就是同步了,現在我們將代碼改成如下:

public class ThreadSafeTest {
public static Map<Integer,Integer> map=new ConcurrentHashMap<>();
public static void main(String[] args) {
    ExecutorService pool1 = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 10; i++) {
        pool1.execute(new Runnable() {
            @Override
            public void run() {
                Random random=new Random();
                int randomNum=random.nextInt(10);
               countRandom(randomNum);
            }
        });
    }
}
public static synchronized void countRandom(int randomNum){
    if(map.containsKey(randomNum)){
        map.put(randomNum,map.get(randomNum)+1);
    }else{
        map.put(randomNum,1);
    }
}
}

上述代碼在當前類中沒有線程安全的問題,但依然有線程安全的危險,成員變量map依然有可能會在其他地方被更改,在java併發中屬於無效的同步鎖

ThreadLoacl線程單例

這個單例可能要畫一個引號了,因爲並不能保證整個應用全局唯一,但是可以保證線程唯一,這麼理解呢?

public class ThreadLoaclInstance {
    private static final ThreadLocal<ThreadLoaclInstance> THREAD_LOACL_INSTANCE_THREAD_LOCAL
            =new ThreadLocal<ThreadLoaclInstance>(){
        @Override
        protected ThreadLoaclInstance initialValue() {
            return new ThreadLoaclInstance();
        }
    };

    private ThreadLoaclInstance(){

    }

    public static ThreadLoaclInstance getInstance(){
        return THREAD_LOACL_INSTANCE_THREAD_LOCAL.get();
    }
}

測試類

/**
 * 應用類
 */
public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        Thread t1=new Thread(()->{
           ThreadLoaclInstance instance=ThreadLoaclInstance.getInstance();
            System.out.println(Thread.currentThread().getName()+" "+instance);
        });
        Thread t2=new Thread(()->{
            ThreadLoaclInstance instance=ThreadLoaclInstance.getInstance();
            System.out.println(Thread.currentThread().getName()+" "+instance);
        });
        t1.start();
        t2.start();
        System.out.println("program end");
    }
}

運行結果

program end
Thread-1 com.design.pattern.creational.singleton.ThreadLoaclInstance@2831008d
Thread-0 com.design.pattern.creational.singleton.ThreadLoaclInstance@59a30351

看到兩個線程運行拿到的對象並不是同一個。現在想象下main本身是一個線程,裏面又開了兩個線程,如果我們在main裏面拿呢?

/**
 * 應用類
 */
public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        System.out.println("main Thread"+ThreadLoaclInstance.getInstance());
        System.out.println("main Thread"+ThreadLoaclInstance.getInstance());
        System.out.println("main Thread"+ThreadLoaclInstance.getInstance());
        System.out.println("main Thread"+ThreadLoaclInstance.getInstance());
        System.out.println("main Thread"+ThreadLoaclInstance.getInstance());
        System.out.println("main Thread"+ThreadLoaclInstance.getInstance());

        Thread t1=new Thread(()->{
           ThreadLoaclInstance instance=ThreadLoaclInstance.getInstance();
            System.out.println(Thread.currentThread().getName()+" "+instance);
        });
        Thread t2=new Thread(()->{
            ThreadLoaclInstance instance=ThreadLoaclInstance.getInstance();
            System.out.println(Thread.currentThread().getName()+" "+instance);
        });
        t1.start();
        t2.start();
        System.out.println("program end");
    }
}

運行結果

main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
main Threadcom.design.pattern.creational.singleton.ThreadLoaclInstance@78308db1
program end
Thread-1 com.design.pattern.creational.singleton.ThreadLoaclInstance@735037e6
Thread-0 com.design.pattern.creational.singleton.ThreadLoaclInstance@1894a1d6

看到main線程中拿到的都是相同的對象,和兩個線程拿到的都不相同,我們看下,ThreadLocal會爲每個線程提供一個獨立的副本,源碼裏面是基於

static class ThreadLocalMap {
...

做的。
ThreadLoacl維持了線程間的隔離,隔離了對數據訪問的衝突,對於多線程資源共享的情況下,我們想象下使用同步鎖,其實就是以時間換空間的方式,因爲要排隊;ThreadLoacl 就是空間換時間的方式,它會創建很多對象,在一個線程下是唯一的,爲每個線程創建了一個對象,線程相互不會影響,這就是基於ThreadLocal的“單例模式”實現方案

單例模式源碼分析

Runtime餓漢式

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
 
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

java.awt.Desktop 容器單例

public static synchronized Desktop getDesktop(){
        if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
        if (!Desktop.isDesktopSupported()) {
            throw new UnsupportedOperationException("Desktop API is not " +
                                                    "supported on the current platform");
        }

        sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
        Desktop desktop = (Desktop)context.get(Desktop.class);

        if (desktop == null) {
            desktop = new Desktop();
            context.put(Desktop.class, desktop);
        }

        return desktop;
    }

spring中AbstractFactoryBean 單例的影子

public final T getObject() throws Exception {
		if (isSingleton()) {
			return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
		}
		else {
			return createInstance();
		}
}

private T getEarlySingletonInstance() throws Exception {
	Class<?>[] ifcs = getEarlySingletonInterfaces();
	if (ifcs == null) {
		throw new FactoryBeanNotInitializedException(
				getClass().getName() + " does not support circular references");
	}
	if (this.earlySingletonInstance == null) {
		this.earlySingletonInstance = (T) Proxy.newProxyInstance(
				this.beanClassLoader, ifcs, new EarlySingletonInvocationHandler());
	}
	return this.earlySingletonInstance;
}

Mybatis中ErrorContext 基於ThreadLocal的單例模式

package org.apache.ibatis.executor;

public class ErrorContext {
    private static final String LINE_SEPARATOR = System.lineSeparator();
    private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal();
    private ErrorContext stored;
    private String resource;
    private String activity;
    private String object;
    private String message;
    private String sql;
    private Throwable cause;

    private ErrorContext() {
    }

    public static ErrorContext instance() {
        ErrorContext context = (ErrorContext)LOCAL.get();
        if (context == null) {
            context = new ErrorContext();
            LOCAL.set(context);
        }

        return context;
    }
   ...

最後

單例模式在面試中必考點,很多面試官喜歡用單例模式來挖掘面試者的深度,希望小夥伴們把單例模式實現方案、優缺點、應用場景、在源碼中的使用、序列化的破壞、反射攻擊的防禦等理解透,拿到滿意的offer。

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