單例模式的幾種簡單實現以及理解

學過設計模式的都知道,在創建型模式中,有個單例模式,很簡單的設計模式,但是,這裏面也設計了比較多的小的細節問題,此處根據代碼,簡單的討論一下個人的理解。

什麼是單例模式?

難道我學習單例模式之前,就必須去學習所謂的設計模式嗎?哈哈,我就不學可以嗎?

作者負責的告訴你,你即使根本不知道什麼是設計模式,也可以很快的精通單例模式,哈哈,開個玩笑,活躍一下氣氛!

所謂的單例模式,顧名思義,其本質就是單例,那什麼是單例呢?很簡單,就是這個類在程序中只能被創建一次,也就是說,無論你有多少個該類的引用,但是該類的對象實例只能存在一個!這裏多說一句,所謂的設計模式,不過是前輩們總結出來的一些開發經驗,或者是一些開發技巧更好的去處理程序個模塊間的耦合性,所以,說到底,設計模式,就是已經有人把一條條開發經驗以結論的形式總結了下來,供後輩直接使用!哈哈,這個總結其實還不錯~~

那我怎麼才能保證這個類的對象實例只有一個呢?

帶着這個問題去思考,先不要着急去看現成的代碼,如果現在沒有單例模式,沒有所謂的設計模式,你現在要滿足一個單例模式的邏輯,你會怎麼辦?順着這個邏輯,我們會想到,當我在任意階段創建該類的對象實例的時候,我都判斷一下該類是不是已經被創建過了。好的,能想到這裏,我們其實已經有了一條死路,哪怕此時的我們並不知道是否行得通。那我們怎麼去判斷這個類有沒有實例對象呢?這好像有點難辦,仔細想想,我們第一次創建一個類實例,有了一個指向該實例的引用,那第二次創建的時候呢?我們怎麼在第二次創建的時候,去判斷一下第一次的這個引用呢?這時候,我們有個簡單的做法,就是開始就有一個引用,然後之後程序中所有使用該類實例的地方,都從這個引用中獲取。這個思路,就好像一個主內存,和無數個副本。好了,這個問題已經有答案了,也就是在類內部維護一個自身引用的成員變量,起到上述所說的目的。那這樣就結束了嗎?那我們怎麼控制這些所謂的副本在創建對象的時候,就必須去訪問主內存呢?這個問題,很快又得到了解決,你程序中要創建對象是吧,好,沒問題,但是我這個單例類不讓你外部創建,什麼意思呢?我把自己的構造方法設置爲私有的,哈哈,你外部創建不了,而我內部的引用可以創建,那這時候你所有的外部的引用想要指向該類實例,就必須從我這個主內存中獲取。而爲了更好的編寫習慣,我們又習慣於把類的成員變量封裝,然後利用一個方法來獲取這個成員變量。

如果你很耐心的讀了上述文字描述,你已經可以自己寫一個單例模式了,而如果你沒有讀,那麼,也毫無所謂,因爲上述的描述只不過是一種引導,是個人在已經瞭解單例模式的情況下,所提供的一種學習方法!

單例模式(男主角登場,哇塞)

簡單的思考過後,我們已經知道,單例模式的三要素:自身引用成員變量,私有構造方法,獲取引用的出口

1.第一種最簡單的實現方法

public class Singleton {
	private static Singleton singleton = null;
	private Singleton(){}
	public static Singleton getSingleton()
	{
		if(singleton == null)
			singleton = new Singleton();
		return singleton;
	}
}

 我們來理解一下細節:首先,對於這三個要素不多說,上述其實已經敘述過了。我們來看一下這幾個問題:

爲什麼singleton是private static的?很簡單,private保證了一種封裝性,外部無法直接對singleton引用做出修改,爲什麼非要封裝呢?考慮這樣一個問題,現在singleton已經指向了一個對象實例,並且外部已經已經有很多引用通過這個內部的singleton引用也都指向了這個唯一的實例,但是,此時如果外部把singleton賦值爲null,哈哈,這時候,調用getSingleton方法竟然又可以創建這樣的一個對象實例!而至於爲什麼是static的,其實很簡單,這和方法有關,我們看到這個方法提供了一個向外面返回引用的出口,而我們又知道,單例模式外部引用想要獲取這個引用的時候,就必須調用這個方法,怎麼才能調用這個方法呢?很簡單,直接使用static的,用類去直接調用,而不是用實例去調用。所以說,方法是static的,那Singleton也就必須是static的了。

2.第二種實現方法

class Singleton2
{	
	//初始化的時候就創建,這時候需要理解一下JVM的類加載過程!
	private static Singleton2 singleton2 = new Singleton2();
	private Singleton2(){}
	public static Singleton2 getSingleton()
	{
		return singleton2;
	}
}

我們來看這種方法和之前的唯一不同之處,就是在聲明引用的時候,就進行了創建實例。這樣做是爲什麼呢?其實很簡單,如果你瞭解JVM的類加載過程,你會知道,對於static的類變量,在其類加載到內存的過程中,就已經把這個類變量按照代碼意願(也就是代碼中的new一個對象實例)進行了賦值(詳細信息,請自行學習類加載的初始化過程,也就是所謂的JVM自動構建的clinit()方法),也這樣做的好處是什麼呢?哈哈,類只會加載一次,而創建對象的語句只會在類加載的初始化階段被執行,說明這種情況,不會存在多線程的同步問題。哈哈,是不是很神奇,沒錯,這種情況下,整個new這個語句永遠只會被執行一次。

3.第三種實現方法

//第三種方法
class Singleton3
{
	private static Singleton3 singleton3 = null;
	private Singleton3(){}
	//對方法同步,保證該方法在同一時刻只能由同一線程執行
	public static synchronized Singleton3 getSingleton3()
	{
		if(singleton3 == null)
			singleton3 = new Singleton3();
		return singleton3;
	}
}

這種方法,沒什麼好說的,簡單粗暴的使用synchronized直接對方法進行同步,也是在解決併發同步問題。

4.第四種實現方法

class Singleton4
{
	//volatile保證了變量的多線程下的可見性!
	private volatile static Singleton4 singleton4 = null;
	private Singleton4(){}
	public static Singleton4 getSingleton4()
	{
		//理解第一個判斷
		//很簡單,避免除第一次進入該方法後的其他任意調用該方法時候的synchronized的性能消耗
		if(singleton4 == null)
			synchronized (Singleton4.class)
			{
				//第二個判斷,就是常規的保證其只能實例化一次
				if(singleton4 == null)
					singleton4 = new Singleton4();
			}
		return singleton4;
	}
}

這種方法,有時候,被稱爲雙檢查實現,很容易看到,代碼進行了兩次是否爲空的判斷。

我們來理解一些細節:volatile保證了多線程下的可見性,也就是隻要有線程更改了引用,其他線程再讀引用的時候,得到的應該是修改後的值,volatile就保證了這一點。

那雙檢查又是爲什麼呢?其實並不是這個兩個檢查有什麼關聯,或者說共同決定了某種邏輯,而是這兩個判斷,分別有自己的存在意義。我們先來看第二個,很容易理解,爲了保證唯一性,每次調用的時候,就要看看是否已經創建過了。那第一個怎麼理解呢,其實,也很簡單,假如我們沒有第一個,那麼假如我們現在已經創建了唯一實例,那任何外部引用再調用這個靜態方法的時候,就一定會先執行synchronized上鎖,然後再去判斷有沒有創建過,這樣一來,除了第一次synchronized真正發揮了作用,其餘所有後續的次數synchronized其實是毫無意義的,這樣就浪費了很多次synchronized的性能消耗,所以說先來一下判斷,如果現在引用已經不爲空了,那就根本沒必要上鎖,再去檢查了,因爲上鎖檢查的結果也還是空,哈哈,每一個小的細節真正理解就,都會很感慨我們人類的聰明智慧!

結語:

看到這裏我們也就明白了一件事情,學習不要死記硬背,有一定的理解,其實更容易去使用,去記憶!上述例子,很簡單,但很多小的細節問題,在設計的時候或許也很巧妙。所以說成長是相對的,但學習是絕對的!

PS:這篇文章只是作爲最基礎的瞭解單例模式的一種學習思路,基於多線程安全的單利模式進階實現,參考另外一篇博客:

https://blog.csdn.net/romantic_jie/article/details/103891855

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