1. 程式人生 > >單例模式實現方式

單例模式實現方式

轉自https://www.cnblogs.com/rjzheng/p/8946889.html

先看一副漫畫:


OK,回顧一下小灰的遭遇,上述漫畫中所提出的那些問題主要有以下三點:

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

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

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

針對上述三個問題有了這篇文章,以一種循序漸進的方式引出最後一種單例設計模式,希望對大家能夠有所幫助。

1.餓漢式

直接上程式碼:

package singleton;

public class Singleton1 {
    private static Singleton1 instance = new Singleton1();

    private Singleton1 (){}

    public static Singleton1 getInstance() {
        return instance;
    }

}

優點就是執行緒安全,缺點很明顯就是浪費記憶體,因為它在類載入的時候就做例項化物件的操作了。於是就有了懶漢式的單例模式

2.懶漢式

2.1懶漢式V1

直接上程式碼:

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加在方法上對效能是大打折扣(synchronized會造成執行緒阻塞),於是乎又提出一種雙重校驗鎖的單例設計模式,既保證了執行緒安全又提高了效能。如下:

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

2.2懶漢式V2

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

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版,程式碼如下:

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:其實列舉單例模式也有類似的效能,但是因為可讀性的原因,並不是最推薦的版本。

2.3懶漢式V3

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

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版誕生了:

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

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

2.4懶漢式V4

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

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

先將

public class LazySingleton3

修改為:

public class LazySingleton3 implements Serializable 

測試類如下:

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版程式碼如下:

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作為單例來實現。如果沒有特殊需求,懶漢式V2足夠用了