【原創】本身動手實現牛逼的單例模式

引言

其實寫這篇文章以前,我猶豫了一下,畢竟單例你們都知道,寫這麼一篇文章會不會讓人以爲老掉牙。後來想一想,就當一種記錄吧。先來一副漫畫吧,以下圖所示
image
ok,咱們回顧下小灰的遭遇,上述漫畫所提出的那些問題主要有如下三點:java

  1. 爲何靜態內部類的單例模式是最推薦的?
  2. 如何在反射的狀況下保證單例?
  3. 如何在反序列化中保證單例?

針對上述三個問題有了這篇文章,以一種按部就班的方式,引出最後一種單例設計模式,但願對你們可以有所幫助。設計模式

單例設計模式

一、餓漢式

這種其實你們都懂,很少說,上代碼。安全

package singleton;

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

	private Singleton1 (){}

	public static Singleton1 getInstance() {
		return instance;
	}

}

優勢就是線程安全啦,缺點很明顯,類加載的時候就實例化對象了,浪費空間。因而乎,就提出了懶漢式的單例模式併發

二、懶漢式

(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版,代碼以下所示code

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

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

總結

本文給出了多個版本的單例模式,供咱們在項目中使用。實際上,咱們在實際項目中通常從懶漢式v二、懶漢式v三、懶漢式v4中,根據實際狀況三選一便可,並非非要選擇懶漢式v4做爲單例來實現。 最後,但願你們有所收穫。

相關文章
相關標籤/搜索