設計模式之----單例模式

什麼是設計模式

設計模式,不是一種知識點,它是前人總結出來的對於特定問題的一些解決方案。
有了設計模式,可以讓代碼變得更加容易理解,同時確保了複用性,可靠性,可擴展性。
當代碼很少時,我們往往體會不出設計模式的價值,但當程序的規模擴大到一定量,設計模式的優勢會明顯的顯現出來。

設計模式的分類

設計模式分爲3類:

  1. 創建型模式(5種)---->用於解決對象創建的過程

單例模式,工廠方法模式,抽象工廠模式,建造者模式,原型模式

  1. 結構型模式(7種)---->把類或對象通過某種形式結合在一起,構成某種複雜或合理的結構

適配器模式,裝飾着模式,代理模式,外觀模式,橋接模式,組合模式,享元模式

  1. 行爲型模式(11種)---->用來解決類或對象之間的交互,更合理的優化類或對象之間的關係

觀察者模式,策略模式,模板模式,責任鏈模式,解析器模式,迭代子模式,命令模式,狀態模式,備忘錄模式,訪問者模式,中介者模式

單例模式(SingleTon)

它的作用是:解決對象創建的問題,控制當前類只能產生一個唯一的一個對象。
我們先來思考以下,要實現這樣的功能,要怎麼做?
首先,必須讓它的構造方法私有化,不能在其他的main函數裏隨便調用。
其次,要獲得這麼一個唯一的對象,我們必須創建出一個對象,那麼創建對象的這個過程,即 new 的這行代碼放在哪裏合適,構造方法裏?不行,因爲我們本來就是要調用構造方法來創建對象,讓構造方法裏面去 new 一個這個類的對象,也就是我要創建這個類的同時,再去創建對象,創建對象調用構造方法,還去創建對象,不停的創建對象,無休止的耗費棧內存,會拋出內存溢出的錯誤java.lang.StackOverflowError(棧內存溢出錯誤),更別說構造方法還要使用private進行修飾,在本類之外調用不到。
那 new 的過程放在代碼塊裏?代碼塊不能有返回值,即使執行了new的過程,我也拿不到創建的那個對象,不行。放在普通方法裏?普通方法可以有返回值,但是每次調用一次方法,都會創建出一個新的對象,不是唯一的對象,還是不行。最後,我們只能放在屬性裏:

public SingleTon singleTon = new SingleTon();

結果顯而易見,還是不行,每次創建SingleTon這個類的對象,都會初始化一個屬性,這個屬性繼續去 new ,new 的過程繼續創建對象,陷入一個死循環,還是會拋出:Exception in thread "main" java.lang.StackOverflowError。這個過程,有點像遞歸,但它不是遞歸,它和遞歸有着異曲同工之妙,遞歸看似是一個方法的調用,但是遞歸執行起來是許多個一樣的方法,是方法執行到一半的時候,調了另外的一個方法,那個方法長得跟它自己一樣,然後這兩個方法的參數不一樣,是這樣的一個過程,第一個方法執行到一半的時候調用了第二個,第一個沒執行完等着,第二個又調了第三個,第二個等着,如果一直執行不完,那個之前的調用就一直等着,那麼會產生無限的內存,最後堆死了。那爲什麼是棧內存溢出而不是堆內存溢出呢?因爲構造方法的執行是要在棧內存裏開闢一塊空間的,如果構造方法無休止的調用,最終一定會產生棧內存溢出的錯誤。

這裏簡單說一下棧內存和堆內存裏面到底存的啥。
棧內存裏存:變量空間,執行過程中的臨時空間,方法執行體。
推內存裏存:通過 new 創建出的對象空間。

那麼接下來該怎麼解決這個問題呢?
很簡單,只需讓那個屬性被static修飾即可,被static修飾過的屬性,在這個類下有且僅有一份,就不會產生棧內存溢出的錯誤了。

public static SingleTon singleTon = new SingleTon();

被static修飾過的屬性還有一個好處,就是不用new,直接類名打點調用即可。
但是,新的問題出現了,既然這個屬性只有一份,它就相當於是一級保護對象了,那麼用public修飾符去修飾,豈不是很危險嗎?我們設想這樣一種情況:

	public static void main(String[] args) {
		//SingleTon singleTon = SingleTon.singleTon;
		//singleTon = null;
		SingleTon.singleTon = null;
	}

直接把我們唯一的singleTon對象給鬧沒了。所以public權限修飾符需要慎用,很危險。
爲了保證安全性,我們把public改成private
同時需要提供一個獲得該私有屬性的共有方法:

	public SingleTon getSingleTon(){
		return singleTon;
	}

問題又來了,我得獲得這個唯一的對象,就得調用這個公用的get方法,那麼,方法怎麼調啊,得先創建對象,對象打點調用。我現在爲了獲取對象,我得先創建對象,而構造方法私有了,我根本沒法創建對象。怎麼辦呢?
我們很自然的可以想到,這個get方法,也應該用static進行修飾,目的是不用創建對象就可以調用方法。(這裏我們要區分,屬性利用static修飾的最大意義是讓它唯一不產生棧內存溢出,而方法利用static修飾的最大意義是不創建對象即可調用)
這樣一來,我們不必擔心這個唯一的對象被隨便賦值爲空的情況:
在這裏插入圖片描述
上面的代碼會報紅線錯誤,提示左邊部分必須是一個變量。
到這裏,單例模式基本就寫完了。
但是,就萬事大吉了嗎?
以上的代碼和思考過程,僅僅是單例模式的一種實現方式:餓漢式。
爲什麼是餓漢式呢?因爲它的屬性在類加載的時候就加載出來了,加載出來以後這個對象馬上就能用。

餓漢式

public class SingleTon {
	private SingleTon(){
	}
	private static SingleTon singleTon = new SingleTon();
	public static SingleTon getSingleTon(){
		return singleTon;
	}
}

餓漢式帶來的問題主要是加載性的問題,假如說我這個類很龐大,加載起來也比較費事,關鍵是我並不是着急的馬上要這個唯一的對象,如果代碼執行了很長時間都沒有用到這個對象,那這個對象就白白浪費了內存空間,並且增加了服務器啓動時的消耗。

懶漢式

懶漢式的意思顧名思義,就是比較懶,不着急去創建那個唯一的實例,什麼時候用,纔去創建對象。
可以做如下修改,將餓漢式改成懶漢式:

public class SingleTon {
	private SingleTon(){
	}
	private static SingleTon singleTon;//沒有創建對象,初始值爲null
	public static SingleTon getSingleTon(){
		if(singleTon == null){
			singleTon = new SingleTon();
		}
		return singleTon;
	}
}

這種寫法,解決了上一種餓漢式的內存浪費問題,但同時也帶來了新的問題,在多線程的情況下,產生線程安全的問題。
在同一時間下,許多人同時併發的調用getSingleTon方法獲取單例對象,如果這個對象還沒有被初始化,那到底是誰先一步讓它new出來,誰後一步讓它new出來,線程安全問題由此產生,我們可以通過添加線程鎖來控制。
在方法上面加鎖:
鎖定的是當前調用方法時的那個對象。

	public static synchronized SingleTon getSingleTon(){
		if(singleTon == null){
			singleTon = new SingleTon();
		}
		return singleTon;
	}

這樣一來可以控制線程安全的問題,但是又帶來了新的問題。
在方法上直接添加了鎖,鎖定的是當前調用方法時的那個對象。相當於兩個人在爭搶對象的使用權,誰搶到了,誰先用,用完了,第二個人再用,但是使用對象的這個過程可能會很慢,整個方法執行的過程中,其他人都等着,由此帶來了性能問題。
增強鎖的性能:鎖類模板+雙重判斷
上面的代碼中,也只有new對象的那行代碼會產生線程衝突,if語句判斷的過程其實不需要加鎖。我們不必爲了這一行代碼而鎖上整個方法,使得性能降低。
爲啥要鎖定當前類的類模板---->我們要鎖的不是對象本身,是創建對象時把用來創建對象的模板給鎖了。
但是還有一個問題,如果if語句判斷的過程不鎖,併發訪問時可能我在判斷的時候你也在判斷,而我要鎖定的時候你已經鎖定了,同樣不能保證線程的安全。我們還需要加一層判斷來確保嚴謹和萬無一失:

	public static SingleTon getSingleTon(){
	//雙重檢測模型實現的單例模式
		if(singleTon == null){
			synchronized (SingleTon.class) {
				if(singleTon == null){
					singleTon = new SingleTon();
				}
			}
		}
		return singleTon;
	}

按理說雙重檢測模型的單例模式已經很完備了,運行起來也足夠穩定,但還有一個更好的建議,在屬性那塊,添加一個volatile修飾符來修飾屬性。

private static volatile SingleTon singleTon;

目的是爲了保證屬性的創建及賦值過程不會產生指令重排序。
怎麼理解呢?
我們不妨先來看看對象是如何產生的:
發送一行代碼 new SingleTon();----->指令
這個指令在內存中做的三件事情是這樣的:

  1. 先開闢內存空間-對象
  2. 對象空間初始化(往對象空間裏面擺放信息)
  3. 將對象空間的地址賦予變量存儲

正常來講,這三個過程是按順序執行的,但是CPU在執行的時候,爲了提升自身的性能,第2步和第3步可以進行順序的變化(也就是說CPU認爲2步和3步可以進行指令重排)。

最終的懶漢式單例模式代碼體現:

public class SingleTon {
	private SingleTon(){
	}
	
	private static volatile SingleTon singleTon;//沒有創建對象,初始值爲null
	
	public static SingleTon getSingleTon(){
		if(singleTon == null){
			synchronized (SingleTon.class) {
				if(singleTon == null){
					singleTon = new SingleTon();
				}
			}
		}
		return singleTon;
	}

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章