【設計模式】你的單例模式真的是生產可用的嗎?

本文重要關注點:html

  • 線程安全的單例模式
  • 防止對象克隆破壞單例模式Singleton
  • 防止序列化破壞單例模式

單例模式

什麼是單例模式

單例模式屬於管理實例的創造型類型模式。單例模式保證在你的應用種最多隻有一個指定類的實例。java

單例模式應用場景

  • 項目配置類

讀取項目的配置信息的類能夠作成單例的,由於只須要讀取一次,且配置信息字段通常比較多節省資源。經過這個單例的類,能夠對應用程序中的類進行全局訪問。無需屢次對配置文件進行屢次讀取。設計模式

  • 應用日誌類

日誌器Logger在你的應用中是無處不在的。也應該只初始化一次,可是能夠處處使用。安全

  • 分析和報告類

若是你在使用一些數據分析工具例如Google Analytics。你就能夠注意到它們被設計成單例的,僅僅初始化一次,而後在用戶的每個行爲中均可以使用。多線程

單例模式簡圖

實現單例模式的類

  • 將默認的構造器設置爲private。阻止其餘類從應用中直接初始化該類。併發

  • 建立一個public static 的靜態方法。該方法用於返回一個單例類實例。oracle

  • 還能夠選擇懶加載初始化更友好。ide

示例代碼

示例代碼參見如下類工具

  • org.byron4j.cookbook.designpattern.singleton.Singleton
public class Singleton {

    private static Singleton instance;

    // 構造器私有化
    private Singleton(){

    }

    // 提供靜態方法
    public static Singleton getInstance(){

        // 懶加載初始化,在第一次使用時才建立實例
        if(instance == null){
            instance = new Singleton();
        }
        return  instance;
    }


    public void display(){
        System.out.println("Hurray! I am create as a Singleton!");
    }


}
複製代碼

單元測試類:性能

package org.byron4j.cookbook.designpattern;

import org.byron4j.cookbook.designpattern.singleton.Singleton;
import org.junit.Test;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingletonTest {

    @Test
    public void test(){
        final Set<Singleton> sets = new HashSet<>();

        ExecutorService es = Executors.newFixedThreadPool(10000);

        for(int i = 1; i <= 100000; i++){
            es.execute(new Runnable() {
                public void run(){
                    Singleton s = Singleton.getInstance();
                    sets.add(s);
                }
            });
        }

        System.out.println(sets);

    }
}

複製代碼

運行輸出以下,結果生成了多個Singleton實例:

[org.byron4j.cookbook.designpattern.singleton.Singleton@46b91344, org.byron4j.cookbook.designpattern.singleton.Singleton@1f397b96]

線程安全的單例模式

線程安全對於單例類來講是很是重要的。上述Singleton類是非線程安全的,由於在線程併發的場景下,可能會建立多個Singleton實例。

爲了規避這個問題,咱們能夠將 getInstance 方法用同步字 synchronized 修飾,這樣迫使線程等待直到前面一個線程執行完畢,如此就避免了同時存在多個線程訪問該方法的場景。

public static synchronized Singleton getInstance() {
		
		// Lazy initialization, creating object on first use
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
}
複製代碼

這樣確實解決了線程安全的問題。可是,synchronized 關鍵字存在嚴重的性能問題。咱們還能夠進一步優化 getInstance 方法,將實例同步,將方法範圍縮小:

public static Singleton getInstance() {

		// Lazy initialization, creating object on first use
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}

	return instance;

}
複製代碼

單元測試三種方式耗時比較:

package org.byron4j.cookbook.designpattern;

import org.byron4j.cookbook.designpattern.singleton.Singleton;
import org.byron4j.cookbook.designpattern.singleton.SingletonSynchronized;
import org.byron4j.cookbook.designpattern.singleton.SingletonSynchronizedOptimized;
import org.junit.Test;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingletonTest {

    @Test
    public void test(){
        final Set<Singleton> sets = new HashSet<>();
        long startTime = System.currentTimeMillis();
        ExecutorService es = Executors.newFixedThreadPool(10000);

        for(int i = 1; i <= 100000; i++){
            es.execute(new Runnable() {
                public void run(){
                    Singleton s = Singleton.getInstance();
                    sets.add(s);
                }
            });
        }
        System.out.println("test用時:" + (System.currentTimeMillis() - startTime));
        System.out.println(sets);

    }

    @Test
    public void testSynchronized(){
        final Set<SingletonSynchronized> sets = new HashSet<>();
        long startTime = System.currentTimeMillis();
        ExecutorService es = Executors.newFixedThreadPool(10000);

        for(int i = 1; i <= 100000; i++){
            es.execute(new Runnable() {
                public void run(){
                    SingletonSynchronized s = SingletonSynchronized.getInstance();
                    sets.add(s);
                }
            });
        }
        System.out.println("testSynchronized用時:" + (System.currentTimeMillis() - startTime));
        System.out.println(sets);

    }

    @Test
    public void testOptimised(){
        final Set<SingletonSynchronizedOptimized> sets = new HashSet<>();
        long startTime = System.currentTimeMillis();
        ExecutorService es = Executors.newFixedThreadPool(10000);

        for(int i = 1; i <= 100000; i++){
            es.execute(new Runnable() {
                public void run(){
                    SingletonSynchronizedOptimized s = SingletonSynchronizedOptimized.getInstance();
                    sets.add(s);
                }
            });
        }

        System.out.println("testOptimised用時:" + (System.currentTimeMillis() - startTime));
        System.out.println(sets);

    }
}

複製代碼

運行測試用例,輸出以下:

test用時:1564
[org.byron4j.cookbook.designpattern.singleton.Singleton@68eae58e]

testSynchronized用時:3658
[org.byron4j.cookbook.designpattern.singleton.SingletonSynchronized@36429a46]

testOptimised用時:2254
[org.byron4j.cookbook.designpattern.singleton.SingletonSynchronizedOptimized@21571826]


複製代碼

能夠看到,最開始的實現方式性能是最好的,可是是非線程安全的; Synchronized 鎖住整個getInstance方法,能夠作到線程安全,可是性能是最差的; 縮小Synchronized範圍,能夠提升性能。

單例Singleton和對象克隆

涉及單例類時還要注意clone方法的正確使用:

package org.byron4j.cookbook.designpattern.singleton;

/** * 單例模式實例 * 1. 構造器私有化 * 2. 提供靜態方法供外部獲取單例實例 * 3. 延遲初始化實例 */
public class SingletonZClone implements Cloneable{

    private static SingletonZClone instance;

    // 構造器私有化
    private SingletonZClone(){

    }

    // 提供靜態方法
    public static SingletonZClone getInstance(){

        // 將同步鎖範圍縮小,下降性能損耗
        if(instance == null){
            synchronized (SingletonZClone.class){
                if(instance == null){
                    instance = new SingletonZClone();
                }
            }
        }
        return  instance;
    }

    /** * 克隆方法--改成public * @return * @throws CloneNotSupportedException */
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public void display(){
        System.out.println("Hurray! I am create as a SingletonZClone!");
    }


}

複製代碼

默認狀況下clone時protected修飾的,這裏改成了public修飾,測試用例以下:

@Test
    public void testClone() throws CloneNotSupportedException {
        SingletonZClone singletonZClone1 = SingletonZClone.getInstance();
        SingletonZClone singletonZClone2 = SingletonZClone.getInstance();
        SingletonZClone singletonZClone3 = (SingletonZClone)SingletonZClone.getInstance().clone();

        System.out.println(singletonZClone1 == singletonZClone2);
        System.out.println(singletonZClone1 == singletonZClone3);
        System.out.println(singletonZClone2 == singletonZClone3);

    }
複製代碼

輸出以下:

true

false

false

咱們瞭解一下clone方法的API解釋, clone 後的對象雖然屬性值多是同樣的,可是已經不是同一個對象實例了:

x.clone() != x

x.clone().getClass() == x.getClass()

x.clone().equals(x)

clone方法返回一個被克隆對象的實例的副本,除了內存地址其餘屬性值都是同樣的,因此副本和被克隆對象不是同一個實例。 能夠看出clone方法破壞了單例類,爲防止該問題出現,咱們須要禁用clone方法,直接改成:

/** * 克隆方法--改成public * @return * @throws CloneNotSupportedException */
    @Override
    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
複製代碼

單例和序列化問題

Java序列化機制容許將一個對象的狀態轉換爲字節流,就能夠很容易地存儲和轉移。 一旦對象被序列化,你就能夠對其進行反序列化--將字節流轉爲對象。 若是一個Singleton類被序列化,則可能建立重複的對象。 咱們可使用鉤子hook,來解釋這個問題。

readResolve()方法

在Java規範中有關於readResolve()方法的介紹:

對於可序列化的和外部化的類,readResolve() 方法容許一個類能夠替換/解析從流中讀取到的對象。 經過實現 readResolve 方法,一個類就能夠直接控制反序列化後的實例以及類型。 定義以下:

ANY-ACCESS-MODIFIER Object readResolve()
       		throws ObjectStreamException;
複製代碼

readResolve 方法會在ObjectInputStream 從流中讀取一個對象時調用。ObjectInputStream 會檢測類是否認義了 readResolve 方法。 若是 readResolve 方法定義了,會調用該方法用於指定從流中反序列化後做爲返回的結果對象。 返回的類型要與原對象的類型一致,否則會出現 ClassCastException。

@Test
    public void testSeria() throws Exception {
        SingletonZCloneSerializable singletonZClone1 = SingletonZCloneSerializable.getInstance();


        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"));
        oos.writeObject(singletonZClone1);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
        SingletonZCloneSerializable test = (SingletonZCloneSerializable) ois.readObject();
        ois.close();
        System.out.println(singletonZClone1 == test);

    }
複製代碼

測試輸出: false; 說明反序列化的時候已經不是原來的實例了,如此會破壞單例模式。

因此咱們能夠覆蓋 readResolve 方法來解決序列化破壞單例的問題:

類 SingletonZCloneSerializableReadResolve 增長 readResolve 方法:

/** * 反序列化時返回instance實例,防止破壞單例模式 * @return */
    protected Object readResolve(){
        return getInstance();
    }
複製代碼

執行測試用例:

@Test
    public void testSReadResolve() throws Exception {
        
         s = SingletonZCloneSerializableReadResolve.getInstance();


        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"));
        oos.writeObject(s);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
        SingletonZCloneSerializableReadResolve test = (SingletonZCloneSerializableReadResolve) ois.readObject();
        ois.close();
        System.out.println(s == test);

    }
複製代碼

輸出true,有效防止了反序列化對單例的破壞。

你知道嗎?

  • 單例類是不多使用的,若是你要使用這個設計模式,你必須清楚的知道你在作什麼。由於全局範圍內僅僅建立一個實例,因此在資源受約束的平臺是存在風險的。

  • 注意對象克隆。 單例模式須要仔細檢查並阻止clone方法。

  • 多線程訪問下,須要注意線程安全問題。

  • 當心多重類加載器,也許會破壞你的單例類。

  • 若是單例類是可序列化的,須要實現嚴格類型

相關文章
相關標籤/搜索