單例設計模式總結-5種常見寫法+防止發射反序列化
單例模式是設計模式中最常見的,也是最簡單的一種,所謂單例,是需要在任何時候只存在一個物件例項,故顯然需要私有化構造器,構造器私有了,要想獲得這個例項,故必須在類內部建立物件例項,同時必須提供靜態方法來獲取,靜態方法只能操作靜態屬性,故內部物件例項需要被static修飾,由於單例,可用final修飾;
單例存在多種寫法,有各自不同的特點,下面介紹常用的寫法,並且這些寫法有些存在漏洞,如發射、發序列化可以破壞該單例;
一、單例模式的五種寫法
1、餓漢式
餓漢式在類載入後,直接建立了物件,從文章開頭的解釋出發,可以理解為什麼用private、static這些關鍵字public class Singleton { private static Singleton instance = new Singleton(); private Singleton(){ } public static Singleton getInstance(){ return instance; } }
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、單例雙檢鎖模式
不加volatile關鍵字的雙檢鎖模式,解決了懶漢式的執行緒安全問題,但它帶來了新的問題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; } }
雙檢鎖模式---存在問題(與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關鍵字的鎖的物件是誰、反射和反序列化的原理,如何預防、類載入機制等等。