天天看點

設計模式之單例模式(Singleton Pattern)結論:

懶漢模式與餓漢模式的差別及優缺點

單例模式的實作:

  • 懶漢模式和餓漢模式——多線程debug
  • 雙重校驗加鎖機制
  • 内部類實作單例模式(延遲加載、線程安全)
  • 序列化對單例模式的破壞
  • 反射對單例類的攻擊
  • 枚舉
  • 無鎖實作單例模式

結論

What:

保證一個類隻有一個執行個體,并提供全局通路點。

Where:

  1. 要求生産唯一序列号。
  2. WEB 中的計數器,不用每次重新整理都在資料庫裡加一次,用單例先緩存起來。
  3. 建立的一個對象需要消耗的資源過多,比如 I/O 與資料庫的連接配接等。

懶漢模式 vs 餓漢模式

懶漢模式:很懶。在調用的時候才會建立單例。(延遲加載)

餓漢模式:很餓。在系統加載的時候就會建立單例。(預加載)

懶漢模式 餓漢模式
優點 第一次調用的時候才初始化,避免記憶體浪費 因為不用加鎖就可以保證線程安全,是以執行效率高
缺點 必須加鎖才能保證線程安全,加鎖則會影響性能 類加載的時候就會初始化,造成記憶體浪費

How:

懶漢模式和餓漢模式示例:

/**
 * 懶漢模式(延遲加載,非線程安全)
 */
1. public class LazySingleton {
2. 
3.     private static LazySingleton lazySingleton = null;
4. 
5.     private LazySingleton() {
6.     }
7. 
8.     public static LazySingleton getInstance() {
9.         if (lazySingleton == null) {
10.             lazySingleton = new LazySingleton();
11.         }
12.         return lazySingleton;
13.     }
14. }
           
/**
 * 餓漢模式(預加載,線程安全)
 */
1. public class HungrySingleton implements Serializable {
2. 
3.     private final static HungrySingleton hungrySingleton = new HungrySingleton();
4. 
5.     private HungrySingleton() {
6.     }
7. 
8.     public static HungrySingleton getInstance() {
9.         return hungrySingleton;
10.     }
11. }
           

以上兩種代碼在單線程的情況下是沒問題的,但懶漢模式在多線程的情況就會有可能出現問題。

*在重制問題之前,首先要學會多線程debug。可以參考 idea 多線程debug

先寫一個簡單的線程類:

1. public class ThreadDemo implements Runnable{
2.     @Override
3.     public void run() {
4.         LazySingleton lazySingleton = LazySingleton.getInstance();
5.         System.out.println(Thread.currentThread().getName() + "  " + lazySingleton);
6.     }
7. }

           
1. public class Main {
2.     public static void main(String[] args) {
3.         new Thread(new ThreadDemo()).start();
4.         new Thread(new ThreadDemo()).start();
5.         System.out.println(Thread.currentThread().getName() + "  " + "is Done!" );
6.     }
7. }
           

首先在類LazySingleton中的第9行設定斷點。分别讓Thread0和Thread1到達斷點。效果如下:

設計模式之單例模式(Singleton Pattern)結論:
設計模式之單例模式(Singleton Pattern)結論:
設計模式之單例模式(Singleton Pattern)結論:
設計模式之單例模式(Singleton Pattern)結論:
設計模式之單例模式(Singleton Pattern)結論:
設計模式之單例模式(Singleton Pattern)結論:

由輸出結果可以看出,多線程的情況下出現單例對象不一緻的情況。

如何寫出一個線程安全的單例模式呢?其實很簡單,使用synchronized加鎖和使用volatile防止重排序。

LazySingleton類修改如下:

public class LazySingleton {

    private volatile static LazySingleton lazySingleton = null;

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        synchronized (LazySingleton.class) {
            if (lazySingleton == null) {
                lazySingleton = new LazySingleton();
            }
        }
        return lazySingleton;
    }
}
           
synchronized關鍵字可以修飾方法,也可以在方法内部作為synchronized塊。如果用synchronized修飾方法對于程式性能是有較大影響的,因為每次進入方法都會加鎖。而在方法内部特定的邏輯使用synchronized塊,靈活性較高,沒有直接用synchronized修飾方法性能的損耗大。

修改後的單例模式是否還能優化呢?接下來介紹雙重校驗加鎖機制。廢話不多說,上代碼!

/**
  * 雙重校驗加鎖(延遲加載,線程安全)
  */
public class LazyDoubleCheckSingleton {

    //由于會發生重排序的情況,是以使用volatile保證建立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;
    }
}
           

雙重校驗加鎖機制采用雙鎖機制,安全且在多線程情況下能保持高性能。

那有沒有一種方法,既可以保證線程安全,又能延遲加載呢?

使用内部類實作單例模式(推薦使用):

public class StaticInnerClassSingleton{
    private StaticInnerClassSingleton() {
    }

    private static class InnerClass {
        private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

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

           

這種方式也是比較推薦使用的,因為實作的難度低。通過内部類實作的單例模式既可以延遲加載,不造成記憶體浪費,又可以不用加鎖保證線程安全,提高執行效率。

序列化對單例模式的破壞

到此為止,是否覺得寫的單例模式完美無缺了呢?接下來看一下的示例代碼:

public class SerializableSingleton {
    private final static SerializableSingleton serializableSingleton = new SerializableSingleton();

    private SerializableSingleton() {
  }

    public static SerializableSingleton getInstance() {
        return serializableSingleton;
    }
}

           
public class SerializableSingletonTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SerializableSingleton instance = SerializableSingleton.getInstance();

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SingletonTest"));
        oos.writeObject(instance);

        File file = new File("SingletonTest");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
	//反序列化擷取SerializableSingleton對象
        SerializableSingleton newInstance = (SerializableSingleton) ois.readObject();

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

輸出結果:

SingletonPattern.HungrySingleton@7f31245a
SingletonPattern.HungrySingleton@568db2f2
false
           

由輸出結果可以看出,通過序列化和反序列化之後兩個的對象是不一樣的,這也違背了單例模式的初衷。但為什麼會這樣呢?我猜測是反序列化的時候執行了什麼代碼重新建立對象導緻的。接下來通過檢視源碼去驗證我的猜測。

反序列化是通過ObjectInputStream類的readObject方法實作的,那就直接打開readObject方法撸源碼吧!

public final Object readObject() throws IOException, ClassNotFoundException{

	//為省篇幅,省略部分代碼
        try {
            Object obj = readObject0(false);
            //為省篇幅,省略部分代碼
            return obj;
        } 
	//為省篇幅,省略部分代碼
    }
           

readObject方法傳回Object對象,那Object obj = readObject0(false);這一行代碼傳回的就是反序列化對象咯,那打開readObject0()方法檢視裡面有什麼乾坤…

/**
     * Underlying readObject implementation.
     */
    private Object readObject0(boolean unshared) throws IOException {

        //為省篇幅,省略部分代碼

        try {
            switch (tc) {

                //為省篇幅,省略部分代碼

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                //為省篇幅,省略部分代碼
            }
        } 
	//為省篇幅,省略部分代碼	
    }
           

由方法的注釋就知道這個方法是反序列化的實作方法。因為我傳進去的是Object對象,是以重點看return checkResolve(readOrdinaryObject(unshared))這一行。

private Object readOrdinaryObject(boolean unshared) throws IOException{
        
	//為省篇幅,省略部分代碼
	
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        //為省篇幅,省略部分代碼

        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) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

           

由于readOrdinaryObject方法傳回的是Object對象,是以根據源碼,Object對象的聲明就在obj = desc.isInstantiable() ? desc.newInstance() : null這一行,我是不是離真相越來越近了呢?接下來繼續看isInstantiable源碼。

/**
     * Returns true if represented class is serializable/externalizable and can
     * be instantiated by the serialization runtime--i.e., if it is
     * externalizable and defines a public no-arg constructor, or if it is
     * non-externalizable and its first non-serializable superclass defines an
     * accessible no-arg constructor.  Otherwise, returns false.
     */
    boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
    }
           

根據方法的注釋可知,如果一個實作了序列化的類在運作的時候被執行個體化,那就傳回true,是以就會執行desc.newInstance(),這個方法就會通過反射調用無參的構造方法建立一個新的對象。是以問題就是在這裡,通過反射傳回一個新對象。那有什麼辦法解決呢?

繼續看readOrdinaryObject方法的源碼,其中obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()就是解決問題的所在。

/**
     * Returns true if represented class is serializable or externalizable and
     * defines a conformant readResolve method.  Otherwise, returns false.
     */
    boolean hasReadResolveMethod() {
        requireInitialized();
        return (readResolveMethod != null);
    }
           

根據方法注釋,我可以知道如果一個類是可序列化或可反序列化的,并且定義了一個方法名為readResolve就會傳回true。如果obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()判斷傳回true就會往下執行Object rep = desc.invokeReadResolve(obj)

invokeReadResolve方法通過反射的方式調用反序列化的類中定義的readResolve方法,并且指派給傳回的變量obj。

是以可以猜測,隻要在序列化、反序列化中的類定義了readResolve方法就可以解決單例模式序列化與反序列化對象不一緻的問題。

改進版SerializableSingleton類:

public class SerializableSingleton implements Serializable {
    private final static SerializableSingleton serializableSingleton = new SerializableSingleton();

    private SerializableSingleton() {

    }

    public static SerializableSingleton getInstance() {
        return serializableSingleton;
    }

    private Object readResolve(){
        return serializableSingleton;
    }
}
           
public class SerializableSingletonTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SerializableSingleton instance = SerializableSingleton.getInstance();

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SingletonTest"));
        oos.writeObject(instance);

        File file = new File("SingletonTest");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));

        SerializableSingleton newInstance = (SerializableSingleton) ois.readObject();

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

    }
}

           

運作結果:

SingletonPattern.SerializableSingleton@7f31245a
SingletonPattern.SerializableSingleton@7f31245a
true
           

小結:

在設計單例模式的時候,一定要考慮類是否需要序列化,如果需要序列化則需要添加readResolve方法傳回單例對象。

反射對單例類的攻擊

雖然上面解決了線程安全,序列化破壞單例模式的問題,但還有一個情況下,單例模式會被破壞,那就是通過反射攻擊。

在講解之前先看一下的示例:

public class ReflectTest {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        //餓漢模式
        HungrySingleton instance = HungrySingleton.getInstance();

        Class reflectObject = HungrySingleton.class;
        //擷取類的構造器
        Constructor constructor = reflectObject.getDeclaredConstructor();
        //設定在使用構造器的時候不執行權限檢查
        constructor.setAccessible(true);
        //通過調用無參構造函數建立對象
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();

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

}

           

輸出結果:

SingletonPattern.HungrySingleton@4554617c
SingletonPattern.HungrySingleton@74a14482
false
           

由輸出結果可以看出,通過反射可以破壞單例模式。那有什麼解決辦法呢?

如果是通過内部類實作單例模式或者是餓漢模式的話,在其私有構造器上添加判斷就行。

譬如:

public class HungrySingleton implements Serializable {

    private final static HungrySingleton hungrySingleton = new HungrySingleton();

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

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

但如果是懶漢模式,這就無法解決反射攻擊單例模式了。

枚舉

事實上,還有一種更加優雅的方式設計單例模式。就是使用枚舉!

用枚舉實作的單例模式,更簡潔,其能夠自動支援序列化機制,絕對防止多次執行個體化,還能保證線程安全。

示例:

public enum EnumSingleton {
    INSTANCE;

    private EnumSingleton() {
    }

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}
           
1. public class Test {
2.     public static void main(String[] args) throws IOException, ClassNotFoundException {
3. 
4.         EnumSingleton instance = EnumSingleton.getInstance();
5. 
6.         Class reflectClass = EnumSingleton.class;
7. 
8.         //例子1:測試序列化、反序列化是否能破壞枚舉單例模式
9.         String fileName = "SingletonTest";
10.         //寫檔案
11.         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName));
12.         oos.writeObject(instance);
13.         File file = new File(fileName);
14.         //讀檔案
15.         ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
16.         EnumSingleton newInstance = (EnumSingleton) ois.readObject();
17.         System.out.println(instance == newInstance);
18. 
19.         //例子2:通過反射調用EnumSingleton無參構造器,測試是否能破壞枚舉單例模式
20.         Constructor constructor = null;
21.         try {
22.             constructor = reflectClass.getDeclaredConstructor();
23.             constructor.setAccessible(true);
24.             EnumSingleton newInstance2 = (EnumSingleton) constructor.newInstance();
25.             System.out.println(instance == newInstance2);
26.         } catch (Exception e) {
27.             e.printStackTrace();
28.         }
29. 
30.         //例子3:通過反射調用EnumSingleton有參構造器,測試是否能破壞枚舉單例模式
31.         Constructor constructor2 = null;
32.         try {
33.             constructor2 = reflectClass.getDeclaredConstructor(String.class, int.class);
34.             constructor2.setAccessible(true);
35.             EnumSingleton newInstance3 = (EnumSingleton) constructor2.newInstance("MuggleLee", 22);
36.             System.out.println(instance == newInstance3);
37.         } catch (Exception e) {
38.             e.printStackTrace();
39.         }
40.     }
41. }
           

輸出結果:

true
java.lang.NoSuchMethodException: SingletonPattern.EnumSingleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at SingletonPattern.SerializableSingletonTest.main(SerializableSingletonTest.java:22)
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at SingletonPattern.SerializableSingletonTest.main(SerializableSingletonTest.java:35)
           

首先看下例子1,輸出結果是true,可以證明,序列化反序列化對枚舉單例沒影響。

接下來看例子2,輸出結果報錯顯示java.lang.NoSuchMethodException。這是因為,枚舉類實際上是會繼承抽象類Enum,而Enum類隻有一個有參構造器 protected Enum(String name, int ordinal),是以通過constructor.newInstance()調用無參構造器是錯誤的。

那例子3是調用有參構造器,為什麼還會報錯呢?看報錯資訊已經很清晰了,Cannot reflectively create enum objects,不能通過反射建立枚舉對象。

通過例子可以證明,使用枚舉類可以保證不會被序列化破壞,還能保證不會受到反射攻擊的影響。那為什麼還能保證線程安全呢?

這和JVM類加載機制有關,static類型的屬性會在類被加載之後被初始化,當一個Java類第一次被真正使用到的時候靜态資源被初始化、由于虛拟機在加載枚舉的類的時候,會使用ClassLoader的loadClass方法,而這個方法使用關鍵字synchronized同步代碼塊保證了線程安全,是以Java類的加載和初始化過程都是線程安全的。

無鎖實作單例模式

以上的幾種建立單例模式都是通過synchronized鎖實作的,那有沒有其他方法,不使用鎖又能保證線程安全呢?

我們可以使用CAS實作單例模式!

public class SingletonByCAS {

    private static AtomicReference<SingletonByCAS> INSTANCE = new AtomicReference();

    public SingletonByCAS() {
    }


    public static SingletonByCAS getInstance() {
        SingletonByCAS singletonByCAS = INSTANCE.get();
        for (; ; ) {
            if (singletonByCAS != null) {
                return singletonByCAS;
            }
            singletonByCAS = new SingletonByCAS();
            if (INSTANCE.compareAndSet(null, singletonByCAS)) {
                return singletonByCAS;
            }
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                SingletonByCAS singletonByCAS = SingletonByCAS.getInstance();
                System.out.println(singletonByCAS);
            }).start();
        }
    }
}

           

輸出結果:

SingletonPattern.SingletonByCAS@2fd04fd1
SingletonPattern.SingletonByCAS@2fd04fd1
...
...
...
SingletonPattern.SingletonByCAS@2fd04fd1
SingletonPattern.SingletonByCAS@2fd04fd1
           

通過使用AtomicXXX包裝類調用compareAndSet方法可以保證線程安全,但通過這種方式實作的單例模式真的好嗎?

使用CAS的好處是不需要通過鎖的方式保證線程安全,但是缺點也很顯然,因為是通過自旋的方式執行,如果一直循環或者執行速度很慢的話,CPU的開銷會非常大。

雖然不推薦這種方式實作單例模式,但也可以更加了解CAS的實作和運用。

結論:

設計單例模式盡量選擇用枚舉的方式,代碼量不大而且能保證線程安全、不會遭到序列化、反射的破壞。

了解更多設計模式:

設計模式系列