天天看點

Java設計模式之單例模式(Singleton)

Java設計模式之單例模式(Singleton)

    稍微有點Java程式設計經驗的人,對于設計模式和單例模式都不會很陌生。因為在很多人面試的時候,就會被問道你知道哪些模式啊?寫個單例模式我看看?另一方面,單例确實用到的地方還不少,比如線程池,資料庫連接配接池,HttpApplication都需要被設計為單例,也就是在全局隻能有一個執行個體,如果它們存在多個執行個體,那這些執行個體建立的對象(thread、datasource等)管理起來就會出現混亂。

    現在呢,我們就來說說單例模式的具體實作方式有哪些。

    相信讀者聽得比較多的就是懶漢模式、餓漢模式。甚至你用一些拼音輸入法打出全拼都能顯示出它們的漢字。

懶漢模式

public class Singleton1 {
	//懶漢模式	具備延遲加載特性
	//初始化類但不建立類執行個體,隻有在使用時才真正建立對象
	//但該方法在多線程環境則有問題
	private static Singleton1 singleton1;

	private Singleton1(){}
	
	public static Singleton1 getInstance(){
		if(singleton1 == null){
			singleton1 = new Singleton1();
		}
		return singleton1;
	}
}
           

    代碼中首先建立一個私有的靜态變量,類型就是我們這個單例類類型,接着建立了一個私有的構造器,隻有建立了這樣一個私有構造器,才能保證該類的執行個體隻能在該類内部進行建立。接下來就是一個擷取執行個體的方法,很簡單,就是判斷執行個體是否為空,如果為空,則建立這樣一個執行個體,否則就說明執行個體已經被建立過了,則直接傳回之前建立過的執行個體即可。這樣這樣一個全局唯一執行個體就建立完了。順着這個類看,看不出一點問題,除了感覺建立私有構造方法和在自己類中聲明自己類類型的變量讓你感覺有點别扭。但是如果我們把建立類的環境放到多線程中去考慮,是不是就不是那麼簡單了。比如,兩個線程同時走到getInstance方法判斷singleton1是否為空的地方,thread1判斷是null,就去建立這個執行個體,此時thread2剛好在thread1 new Singleton1()時,對singleton1進行判斷,此時它仍是空,也去執行singleton1 = new Singleton1() 這段代碼,那就相當于得到而兩個不同的執行個體。

    發現了這個問題,解決起來也很簡單,對吧, 也就是在getInstance方法上加上synchronized 關鍵字就行了。也就是下面這種形式

懶漢模式(改進版1)

public class Singleton2 {
	private static Singleton2 singleton2;
	private Singleton2(){}
	public static synchronized Singleton2 getInstance(){
		if(singleton2 == null){
			singleton2 = new Singleton2();
		}
		return singleton2;
	}
}
           

 這段代碼滿足能建立一個全局唯一執行個體,不存線上程安全問題,是可以用在實際項目中去了。但是我們細想一下,我們執行個體化對象的過程隻會執行一次,也就是singleton2=new Singleton2() 代碼實際隻會執行一次,因為之後的方法調用就不會進入if代碼塊中,但是因為我們在方法上加了synchronized ,是以每次方法調用,該方法都會被加鎖,都知道j加了synchronizd關鍵字的方法性能上就沒有那麼高。再往深處想, 如果該方法會被頻繁調用,那麼性能問題就會很突出了。是以有了下面的改進版代碼。

懶漢模式(改進版2)

public class Singleton3 {
	private static  Singleton3 singleton3;
	private Singleton3(){}
	public static Singleton3 getInstance(){
		if(singleton3 == null){
			synchronized (Singleton3.class) {
				if(singleton3==null){
					singleton3 = new Singleton3();
				}
			}
		}
		return singleton3;
	}
}
           

 這段代碼與上面代碼的不同之處:聲明的singleton3變量之前加了個volatile關鍵字,在getInstance方法中,使用了同步代碼塊,volatile機制,我們不展開,隻是說明被volatile修飾的變量,在多線程環境下,如果一旦值被修改,修改後的值在其他線程中會立馬得到重新整理。同步代碼塊與方法使用synchronized修飾的差別,相信讀者都明白,就是讓被上鎖的代碼少一些。使用同步點塊有一個好處,就是隻有建立的代碼才會被上鎖。如果執行個體已經建立,則直接傳回執行個體,與上面每次調用getInstance方法都要加鎖相比,已經好了很多。上面這種建立執行個體的方法也是比較有名的“雙重檢查鎖”模式,這種方式本身是沒有任何問題的,近乎于完美,但是限于Java本身版本的問題,Java版本小于1.5的話,這種雙重檢查鎖機制會失效。是以如果jdk是1.5之前的版本,不要使用這種方式。

餓漢模式

public class Singleton4 {
	private static Singleton4 singleton4 = new Singleton4();
	private Singleton4(){}
	public  static Singleton4 getInstance(){
		return singleton4;
	}

}
           

    這段代碼也比較簡單,與基礎版懶漢模式的差別是,餓漢模式在聲明類類型變量時,就完成了對象的執行個體化,在getInstance方法中,直接傳回執行個體對象,這樣是這種寫法被稱為餓漢模式的原因,因為它急切希望得到執行個體。這種寫法可以生成全局唯一執行個體,由于classloader機制(static修飾的變量),也不存線上程安全問題。該寫法中,隻要初始化類,就會執行個體化singleton4。不管這個執行個體有沒有被用到,這也就是該寫法的一點瑕疵,不具備延遲加載的特性(隻有在使用時,才執行個體化)。

餓漢模式(變種,但并不是改進)

public class Singleton5 {
	private static Singleton5  singleton5 = null;
	static{
		singleton5 = new Singleton5();
	}
	private Singleton5(){}
	public static Singleton5 getInstnce(){
		return singleton5;
	}
}
           

    這種寫法與上面的寫法存在同樣的問題,在類初始化時,對象就被建立出來了,而不管它被建立出來有沒有用。

除了上面的寫法,我通過查資料,看到還有一些其他的單例模式代碼的實作,我們也來分析一下。

靜态内部類

public class Singleton7 {
	private static class SingletonHelper{
		private static final Singleton7 singleton7 = new Singleton7();
	}
	private Singleton7(){}
	public static Singleton7 getInstance(){
		return SingletonHelper.singleton7;
	}
}
           

    如代碼所示,在Singleton7類内部又建立了一個靜态内部類,靜态内部類中聲明了一個static final 修飾的Singleton7 變量,并進行了執行個體化。在getInstance方法中,則直接擷取該内部類中成員變量singleton7。這種寫法也依賴于JVM機制,確定無線程安全問題,可以建立全局唯一執行個體,由于未使用synchronized關鍵字,性能也較好,是一種不錯的實作方法。

枚舉實作

public enum Singleton6 {
	singleton6
	//......其他方法
}
           

    該寫法簡單明了,且不存線上程安全的問題,另外,它也可避免使用反序列化重新建構新的對象。由于jdk1.5才引入枚舉類型,是以對于jdk版本小于1.5的就不适用了。

    以上就是關于單例模式實作方式的總結。另外本人從《Head First設計模式》中了解到說垃圾回收器會把單例對象“吃”掉,書中解釋道,該bug存在于jdk1.2之前的版本,在1.2之後就已經被修正了,是以不需要擔心這個問題。

繼續閱讀