1. 程式人生 > >單例設計模式總結-5種常見寫法+防止發射反序列化

單例設計模式總結-5種常見寫法+防止發射反序列化

單例模式是設計模式中最常見的,也是最簡單的一種,所謂單例,是需要在任何時候只存在一個物件例項,故顯然需要私有化構造器,構造器私有了,要想獲得這個例項,故必須在類內部建立物件例項,同時必須提供靜態方法來獲取,靜態方法只能操作靜態屬性,故內部物件例項需要被static修飾,由於單例,可用final修飾;

單例存在多種寫法,有各自不同的特點,下面介紹常用的寫法,並且這些寫法有些存在漏洞,如發射、發序列化可以破壞該單例;

一、單例模式的五種寫法

1、餓漢式

public class Singleton {
	private static Singleton instance = new Singleton();
	private Singleton(){
	}
	public static Singleton getInstance(){
		return instance;
	}
}
餓漢式在類載入後,直接建立了物件,從文章開頭的解釋出發,可以理解為什麼用private、static這些關鍵字

2、懶漢式

class Singleton2{
	private static Singleton2 instance;
	private Singleton2(){}
	public static Singleton2 getInstance(){
		if(instance==null){ 		//A位置
			instance = new Singleton2();
		}
		return instance;
	}
}
懶漢式具有延遲載入的特點,即在需要用該物件的時候才會建立例項,在一定程度上可以節約點資源;

懶漢式vs餓漢式:餓漢式在類載入後直接建立物件(即使不需要使用物件),所以執行緒安全

懶漢式在需要使用時才會建立物件,非執行緒安全
工程中:建議使用餓漢式

懶漢式的問題:併發訪問時,T1執行到A處暫停,T2同樣執行到A處,並繼續往下執行,
T2例項化了instance,T2執行完,T1執行緒繼續執行,此時T1執行緒會繼續執行instance = new Singleton2();
無法保證單例

3、單例雙檢鎖模式

class Singleton3{
	private static volatile Singleton3 instance; //注意此處volatile關鍵字
	private Singleton3(){}
	public static /*synchronized*/ Singleton3 getInstance(){
		if(instance==null){  //A位置
			synchronized (Singleton3.class) {
				if(instance==null){
					instance=new Singleton3();// B位置
				}
			}
		}
		return instance;
	}
}
不加volatile關鍵字的雙檢鎖模式,解決了懶漢式的執行緒安全問題,但它帶來了新的問題

雙檢鎖模式---存在問題(與Java記憶體模型有關)
理論上時很完美的,但是實際會因Java記憶體模型,設計指令重排序,出現問題
/**
 * 
 * 一、好處:避免在函式上使用synchronized關鍵字,導致每次調getIstance()函式都要
 * 讀鎖的開銷,提高效率
 * 二、潛在問題:
 * instance=new Singleton3();分為3步
 * 1)申請空間
 * 2)初始化空間的值
 * 3)將引用instance指向該空間
 * 分析:實際應該讓3步按照順序來,但由於Java記憶體模型,允許他們不按順序執行,試想:
 * T1執行B處時,初始化時按照1)->3)->2)的順序,剛好執行完3)就被中斷了,
 * 此時,T2執行到A處,判斷instance==null發現instance不為null,於是
 * 將該物件返回,而該物件並未被初始化,這就導致了問題
 * 三、解決之道:單例類的成員用volatile關鍵字修飾,內部原理參考另一篇部落格

4、靜態內部類方式

class Singleton4{
	private Singleton4(){}
	private static class Singleton4Holder{
		private static Singleton4 instance = new Singleton4();
	}
	public static Singleton4 getInstance(){
		return Singleton4Holder.instance;
	}	
}

具有延遲載入特性,同時也是執行緒安全的,是比較推薦的寫法
 1.Singleton4類被載入的時候,並不會例項化instance物件
 2.只有在呼叫getInstance()函式的時候,才開始載入Singleton4Holder類,並建立instance例項

5、列舉

enum Singleton5{
	INSTANCE;
	public void dosomething(){}
}
使用列舉方法的好處在於:
1.列舉天生就是執行緒安全的,其在任意情況下都是單例
2.列舉具有防止反射和發序列化的特點


二、單例模式防止反射和反序列化

1、防止發射,我們知道,可以通過發射方式來獲取類的構造方法,並用紙建立物件,即便構造方法為private修飾的,為了防止發射的漏洞,只需在建構函式內部做個判斷,如下:

	private Singleton(){
		if(null!=instance){
			throw new RuntimeException("單例已經存在");
		}
	}
2、防止反序列化

反序列化:即強物件寫入磁碟再讀入記憶體,得到一個新的例項,破壞了了單例的唯一性
  Java提供了readResolve()方法,可以讓開發者控制物件的反序列化
  解決反序列化方法:在單例類中加入方法


  本質:無論是實現Serializable介面,或是Externalizable介面,當從I/O流中讀取物件時,readResolve()方法都會被呼叫到。  
  實際上就是用readResolve()中返回的物件直接替換在反序列化過程中建立的物件。

private Object readResolve() throws ObjectStreamException {
		return instance;
	}
3、以餓漢式為例,設計防止發射和反序列化漏洞的單例

class Singleton6 implements Serializable{
	private static Singleton6 instance = new Singleton6();
	//防止反射破壞單例
	private Singleton6(){
		if(null!=instance){
			throw new RuntimeException("單例已經存在");
		}
	}
	//防止反序列化破壞單例
	private Object readResolve() throws ObjectStreamException {
		return instance;
	}
	public static Singleton6 getInstance(){
		return instance;
	}
}

4、以靜態內部類方式為例,設計防止反射和反序列化的單例

class Singleton7 implements Serializable{
	//防止反射破壞單例模式
	private Singleton7(){
		if(null!=SingletonHolder.instance){
			throw new RuntimeException("單例已存在");
		}
	}
	//防止反序列化破壞單例模式
    private Object readResolve() throws ObjectStreamException {    
        return SingletonHolder.instance;
    }  
	
	private static class SingletonHolder{
		private static Singleton7 instance = new Singleton7();
	}
	public static Singleton7 getInstance(){
		return SingletonHolder.instance;
	}
}

總結:實際中,需要根據需要,選擇合適的單例型別,從上面可以看出,一個單例涉及的知識點還是挺多的,如volatile關鍵字的原理和作用、執行緒安全問題、synchronized關鍵字的鎖的物件是誰、反射和反序列化的原理,如何預防、類載入機制等等。