1. 程式人生 > >設計模式——單例模式的幾種實現方式

設計模式——單例模式的幾種實現方式

1.餓漢式:靜態常量

這種方法非常的簡單,因為單例的例項被宣告成static和final變量了,在第一次載入類到記憶體中時就會初始化,所以會建立例項本身是執行緒安全的。

<span style="font-family:Microsoft YaHei;font-size:14px;">public class Singleton1 {

	private final static Singleton1 instance = new Singleton1();
	private Singleton1(){}
	public static Singleton1 getInstance(){
		return instance;
	}
}</span>
它的缺點是不是一種懶載入,單例會在載入類後一開始就被初始化,即使客戶端沒有呼叫getInstance()方法。餓漢式的建立方式在一些場景中將無法使用:比如Singleton例項的建立是依賴引數或者配置檔案的,在getInstance()之前必須呼叫某個方法設定引數給它,那麼單例寫法就無法使用了。

2.懶漢式:執行緒不安全

<span style="font-family:Microsoft YaHei;font-size:14px;">public class Singleton3 {
	
	private static Singleton3 instance ;
	private Singleton3(){}
	public  static Singleton3 getInstance(){
		if(instance == null){
			instance = new Singleton3();
		}
		return instance;
	}
}
</span>
這裡使用了懶載入模式,但是卻存在致命的問題。當多個執行緒並行呼叫getInstance()的時候,就會建立多個例項,即在多執行緒下不能正常工作。
3.懶漢式:執行緒安全
<span style="font-family:Microsoft YaHei;font-size:14px;">public class Singleton4 {

	private static Singleton4 instance;
	private Singleton4(){}
	public static synchronized Singleton4 getInstance(){
		if(instance == null){
			instance = new Singleton4();
		}
		return instance;
	}
}</span>
雖然做到了執行緒安全,並且解決了多例項的問題,但是它並不高效。因為在任何時候只能有一個執行緒呼叫getInstance()方法,但是同步操作只需要在第一次呼叫時才被需要,即第一次建立單例例項物件時。
4.懶漢式:靜態內部類
<span style="font-family:Microsoft YaHei;font-size:14px;">public class Singleton5 {

	private static class SingletonHandler{
		private static final Singleton5 INSTANCE = new Singleton5();
	}
	private Singleton5(){}
	public static Singleton5 getInstance(){
		return SingletonHandler.INSTANCE;
	}
}</span>
這種寫法仍然使用JVM本身機制保證了執行緒安全問題;由於SingletonHandler是私有的,除了getInstacne()之外沒有辦法訪問它,因此它是懶漢式的;同時讀取例項的時候不會進行同步,沒有效能缺陷,也不依賴JDK版本。

5.雙重檢驗鎖:

雙重檢驗模式,是一種使用同步塊加鎖的方法。又稱其為雙重檢查鎖,因為會有兩次檢查instance == null,一次是在同步塊外,一次是在同步快內。為什麼在同步塊內還要檢驗一次,因為可能會有多個執行緒一起進入同步塊外的if,如果在同步塊內不進行二次檢驗的話就會生成多個例項了。

<span style="font-family:Microsoft YaHei;font-size:14px;">public class Singleton6 {
	
	private static Singleton6 instance;
	private Singleton6(){}
	public static Singleton6 getSingleton6(){
		if(instance==null){
			synchronized(Singleton6.class){
				if(instance==null){
				   instance = new Singleton6();
				}
			}
		}
		return instance;
	}
	
	
}</span>
其中,instance = new Singleton6()並非是原子操作,事實上在JVM中這句話做了三件事:

1.給instance分配記憶體

2.呼叫Singleton6的建構函式來初始化成員變數

3.將instance物件指向分配的空間(執行完這一步instance就為null)

但是在JVM的即時編譯器中存在指令重排序的優化,也就是說上面的第二步和第三步是不能保證順序的,最終執行的順序可能是1-2-3或者是1-3-2。如果是後者,則在3執行完畢,2執行之前,被執行緒2搶佔了,這時instance已經是非null了(但卻沒有初始化),所以執行緒2會直接返回instance,然後使用,然後會報錯。

<span style="font-family:Microsoft YaHei;font-size:14px;">public class Singleton6 {
	
	private volatile static Singleton6 instance;
	private Singleton6(){}
	public static Singleton6 getSingleton6(){
		if(instance==null){
			synchronized(Singleton6.class){
				if(instance==null){
				   instance = new Singleton6();
				}
			}
		}
		return instance;
	}
	
	
}
</span>
有人認為使用volatile的原因是可見性,也就是可以保證執行緒在本地不會存有instance副本,每次都是去主記憶體中讀取,但是其實是不對的。使用volatile的主要原因是其另一個特性:禁止指令重排序優化。也就是說,在volatile變數的賦值操作後面會有一個記憶體屏障(生成的彙編程式碼上),讀操作不會被重排序到記憶體屏障之前。比如上面的例子,取操作必須在執行完1-2-3之後或者1-3-2之後,不存在執行到1-3然後取到值的情況。從[先行發生原則]的角度理解的話,就是對於一個volatile變數的寫操作都先行發生於後面對這個變數的讀操作。

注意:在Java5以前的版本使用了volatile的雙檢鎖還是有問題的。其原因是Java5以前的JMM(Java記憶體模型)是存在缺陷的,即時將變數宣告成volatile也不能避免重排序。

6.列舉:

<span style="font-family:Microsoft YaHei;font-size:14px;">public class Singleton7 {

	public enum EasySingleton{
		INSTANCE;
	}
}
</span>
我們可以通過EasySingleton.INSTANCE來訪問例項,這比呼叫getInstance()方法簡單多了。建立列舉預設就是執行緒安全,而且還能防止反序列化導致重新建立新的物件。