1. 程式人生 > >單例的兩種實現方式、多個版本及利弊對照

單例的兩種實現方式、多個版本及利弊對照

        單例設計模式,顧明思議,只有一個例項,先交代重要一點,為防止外界對該類進行例項化,需要把類的建構函式宣告為私有的,這樣大家對原理理解更深入些。

1、餓漢式

餓漢模式單例程式碼,經典,可用,無需改進。

package com.single;

/**
 * Created by Liuxd on 2018-11-09.
 */
public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }

    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();

        System.out.println(singleton1 == singleton2);
    }

}

輸出結果 

 true

優點:執行緒安全;

缺點:類載入的時即例項化物件,浪費空間。

於是,就提出了懶漢式的單例模式 ,即太懶,不著急先例項化,什麼時候用到,才去例項化,足夠懶!

2、懶漢式

(1)懶漢式v1(不推薦)

package com.single;

/**
 * Created by Liuxd on 2018-11-09.
 */
public class LazySingleton1 {
    private static LazySingleton1 instance;

    private LazySingleton1() {
    }

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

然而這一版本的單例,執行緒是不安全的;於是為了執行緒安全,第一想法就是在getInstance()方法上加synchronized修飾符,於是getInstance()方法如下所示:

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

改進版本,加鎖,不推薦使用:

然而,新的問題來了:將synchronized加在方法上效能大打折扣(syncrhonized會造成執行緒阻塞),於是乎又提出一種雙重校驗鎖的單例設計模式,既保證了執行緒安全,又提高了效能。雙重校驗鎖的getInstance()方法如下所示

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

(2)懶漢式v2(借用靜態內部類實現)

懶漢式v1的最後一個雙重校驗鎖版,不管效能再如何優越,還是使用了synchronized修飾符,既然使用了該修飾符,那麼對效能多多少少都會造成一些影響,於是乎懶漢式v2版誕生。不過在講該版之前,我們先來複習一下內部類的載入機制,程式碼如下

package test;
public class OuterTest {
    static {
        System.out.println("load outer class...");
    }
    // 靜態內部類
    static class StaticInnerTest {
        static {
            System.out.println("load static inner class...");
        }
        static void staticInnerMethod() {
            System.out.println("static inner method...");
        }
    }
    public static void main(String[] args) {
        OuterTest outerTest = new OuterTest(); // 此刻其內部類是否也會被載入?
        System.out.println("===========分割線===========");
        OuterTest.StaticInnerTest.staticInnerMethod(); // 呼叫內部類的靜態方法
    }
}

輸出如下

load outer class...
===========分割線===========
load static inner class...
static inner method

因此,我們有如下結論

  1. 載入一個類時,其內部類不會同時被載入。

  2. 一個類被載入,當且僅當其某個靜態成員(靜態域、構造器、靜態方法等)被呼叫時發生。。

基於上述結論,我們有了基於靜態內部類的懶漢式V2版,程式碼如下所示:

package singleton;
public class LazySingleton2 {
    private LazySingleton2() {
    }

    static class SingletonHolder {
        private static final LazySingleton2 instance = new LazySingleton2();
    }

    public static LazySingleton2 getInstance() {
        return SingletonHolder.instance;
    }

}

由於物件例項化是在內部類載入的時候構建的,因此該版是執行緒安全的(因為在方法中建立物件,才存在併發問題,靜態內部類隨著方法呼叫而被載入,只加載一次,不存在併發問題,所以是執行緒安全的)

另外,在getInstance()方法中沒有使用synchronized關鍵字,因此沒有造成多餘的效能損耗。當LazySingleton2類載入的時候,其靜態內部類SingletonHolder並沒有被載入,因此instance物件並沒有構建。

而我們在呼叫LazySingleton2.getInstance()方法時,內部類SingletonHolder被載入,此時單例物件才被構建。因此,這種寫法節約空間,達到懶載入的目的,該版也是眾多部落格中的推薦版本。

ps:其實列舉單例模式也有類似的效能,但是因為可讀性的原因,並不是最推薦的版本。

(3)懶漢式v3

然而,懶漢式v2版在反射的作用下,單例結構是會被破壞的,測試程式碼如下所示

package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import singleton.LazySingleton2;
/**
 * @author zhengrongjun
 */
public class LazySingleton2Test {
    public static void main(String[] args) {
        //建立第一個例項
        LazySingleton2 instance1 = LazySingleton2.getInstance();
        //通過反射建立第二個例項
        LazySingleton2 instance2 = null;
        try {
            Class<LazySingleton2> clazz = LazySingleton2.class;
            Constructor<LazySingleton2> cons = clazz.getDeclaredConstructor();
            cons.setAccessible(true);
            instance2 = cons.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        //檢查兩個例項的hash值
        System.out.println("Instance 1 hash:" + instance1.hashCode());
        System.out.println("Instance 2 hash:" + instance2.hashCode());
    }
}

輸出如下

Instance 1 hash:1694819250
Instance 2 hash:1365202186

根據雜湊值可以看出,反射破壞了單例的特性,因此懶漢式V3版誕生了

package singleton;
public class LazySingleton3 {

    private static boolean initialized = false;

    private LazySingleton3() {
        synchronized (LazySingleton3.class) {
            if (initialized == false) {
                initialized = !initialized;
            } else {
                throw new RuntimeException("單例已被破壞");
            }
        }
    }

    static class SingletonHolder {
        private static final LazySingleton3 instance = new LazySingleton3();
    }

    public static LazySingleton3 getInstance() {
        return SingletonHolder.instance;
    }

}

此時再執行一次測試類,出現如下提示

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 test.LazySingleton3Test.main(LazySingleton3Test.java:21)
Caused by: java.lang.RuntimeException: 單例已被破壞
    at singleton.LazySingleton3.<init>(LazySingleton3.java:12)
    ... 5 more
Instance 1 hash:359023572

這裡就保證了,反射無法破壞其單例特性

(4)懶漢式v4

在分散式系統中,有些情況下你需要在單例類中實現 Serializable 介面。這樣你可以在檔案系統中儲存它的狀態並且在稍後的某一時間點取出。

讓我們測試這個懶漢式V3版在序列化和反序列化之後是否仍然保持單例。

先將

public class LazySingleton3

修改為

public class LazySingleton3 implements Serializable 

上測試類如下

package test;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import singleton.LazySingleton3;
public class LazySingleton3Test {
    public static void main(String[] args) {
        try {
            
            LazySingleton3 instance1 = LazySingleton3.getInstance();
            
            ObjectOutput out = null;
            out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
            out.writeObject(instance1);
            out.close();

            //deserialize from file to object
            ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
            LazySingleton3 instance2 = (LazySingleton3) in.readObject();
            in.close();

            System.out.println("instance1 hashCode=" + instance1.hashCode());
            System.out.println("instance2 hashCode=" + instance2.hashCode());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

輸出如下

instance1 hashCode=2051450519
instance2 hashCode=1510067370

顯然,我們又看到了兩個例項類。為了避免此問題,我們需要提供 readResolve() 方法的實現。readResolve()代替了從流中讀取物件。這就確保了在序列化和反序列化的過程中沒人可以建立新的例項。

因此,我們提供懶漢式V4版程式碼如下

package singleton;
import java.io.Serializable;
public class LazySingleton4 implements Serializable {
    private static boolean initialized = false;

    private LazySingleton4() {
        synchronized (LazySingleton4.class) {
            if (initialized == false) {
                initialized = !initialized;
            } else {
                throw new RuntimeException("單例已被破壞");
            }
        }
    }

    static class SingletonHolder {
        private static final LazySingleton4 instance = new LazySingleton4();
    }

    public static LazySingleton4 getInstance() {
        return SingletonHolder.instance;
    }

    private Object readResolve() {
        return getInstance();
    }

}

此時,在執行測試類,輸出如下

instance1 hashCode=2051450519
instance2 hashCode=2051450519

這表示此時已能保證序列化和反序列化的物件是一致的

總結

本文給出了多個版本的單例模式,供我們在專案中使用。實際上,我們在實際專案中一般從懶漢式v2、懶漢式v3、懶漢式v4中,根據實際情況三選一即可,並不是非要選擇懶漢式v4作為單例來實現。最後,希望大家有所收穫。

問題:

  1. 為什麼靜態內部類的單例模式是最推薦的?

  2. 如何在反射的情況下保證單例?

  3. 如何在反序列化中保證單例?