1. 程式人生 > >【原創】自己動手實現牛逼的單例模式

【原創】自己動手實現牛逼的單例模式

pre adr 關鍵字 LV trace ack 也會 code exceptio

引言

其實寫這篇文章之前,我猶豫了一下,畢竟單例大家都知道,寫這麽一篇文章會不會讓人覺得老掉牙。後來想想,就當一種記錄吧。先來一副漫畫吧,如下圖所示
技術分享圖片
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、懶漢式

(1)懶漢式v1

package singleton;

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

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

(3)懶漢式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作為單例來實現。
最後,希望大家有所收獲。

【原創】自己動手實現牛逼的單例模式