設計模式 構建型 單例模式

2021-09-11 08:04:48 字數 3374 閱讀 4869

先來看一下懶漢式單例的實現方式。

把構造器改為私有的,這樣能夠防止被外部的類呼叫。

// version 1.0

public class singleton

public static singleton getinstance()

return instance;

}}

每次獲取instance之前先進行判斷,如果instance為空就new乙個出來,否則就直接返回已存在的instance。

這種寫法在大多數的時候也是沒問題的。問題在於,當多執行緒工作的時候,如果有多個執行緒同時執行到if (instance == null),都判斷為null,那麼兩個執行緒就各自會建立乙個例項——這樣一來,就不是單例了。

那既然可能會因為多執行緒導致問題,那麼加上乙個同步鎖吧!

修改後的**如下,相對於version1.0,只是在方法上多加了乙個synchronized:

// version 2 

public class singleton

public static synchronized singleton getinstance()

return instance;

}}

加上synchronized關鍵字之後,getinstance方法就會鎖上了。如果有兩個執行緒(t1、t2)同時執行到這個方法時,會有其中乙個執行緒t1獲得同步鎖,得以繼續執行,而另乙個執行緒t2則需要等待,當第t1執行完畢getinstance之後(完成了null判斷、物件建立、獲得返回值之後),t2執行緒才會執行執行。——所以這端**也就避免了version1.0中,可能出現因為多執行緒導致多個例項的情況。

但是,這種寫法也有乙個問題:給gitinstance方法加鎖,雖然會避免了可能會出現的多個例項問題,但是會強制除t1之外的所有執行緒等待,實際上會對程式的執行效率造成負面影響。

改進後的**vsersion3如下:

// version 3 

public class singleton

public static singleton getinstance() }}

return instance;

}}

這個版本的**看起來有點複雜,注意其中有兩次if (instance == null)的判斷,這個叫做『雙重檢查 double-check』。

第乙個if (instance == null),其實是為了解決version2中的效率問題,只有instance為null的時候,才進入synchronized的**段——大大減少了機率。

第二個if (instance == null),則是跟version2一樣,是為了防止可能出現多個例項的情況。

還是有小概率出現問題的。

這弄清楚為什麼這裡可能出現問題,首先,我們需要弄清楚幾個概念:原子操作、指令重排。

知識點:什麼是原子操作?

簡單來說,原子操作(atomic)就是不可分割的操作,在計算機中,就是指不會因為執行緒排程被打斷的操作。

比如,簡單的賦值是乙個原子操作:

m = 6; // 這是個原子操作

假如m原先的值為0,那麼對於這個操作,要麼執行成功m變成了6,要麼是沒執行m還是0,而不會出現諸如m=3這種中間態——即使是在併發的執行緒中。

而,宣告並賦值就不是乙個原子操作:

int n = 6; // 這不是乙個原子操作

對於這個語句,至少有兩個操作:

①宣告乙個變數n

②給n賦值為6

——這樣就會有乙個中間狀態:變數n已經被宣告了但是還沒有被賦值的狀態。

——這樣,在多執行緒中,由於執行緒執行順序的不確定性,如果兩個執行緒都使用m,就可能會導致不穩定的結果出現。

知識點:什麼是指令重排?

簡單來說,就是計算機為了提高執行效率,會做的一些優化,在不影響最終結果的情況下,可能會對一些語句的執行順序進行調整。

比如,這一段**:

int a ; // 語句1

a = 8 ; // 語句2

int b = 9 ; // 語句3

int c = a + b ; // 語句4

正常來說,對於順序結構,執行的順序是自上到下,也即1234。

但是,由於指令重排的原因,因為不影響最終的結果,所以,實際執行的順序可能會變成3124或者1324。

由於語句3和4沒有原子性的問題,語句3和語句4也可能會拆分成原子操作,再重排。

——也就是說,對於非原子性的操作,在不影響最終結果的情況下,其拆分成的原子操作可能會被重新排列執行順序。

了解了原子操作和指令重排的概念之後,我們再繼續看version3**的問題。

主要在於instance = new singleton()這句,這並非是乙個原子操作,事實上在 jvm 中這句話大概做了下面 3 件事情。

給 singleton 分配記憶體

呼叫 singleton 的建構函式來初始化成員變數,形成例項

將instance 物件指向分配的記憶體空間(執行完這步 singleton才是非 null 了)

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

再稍微解釋一下,就是說,由於有乙個『instance已經不為null但是仍沒有完成初始化』的中間狀態,而這個時候,如果有其他執行緒剛好執行到第一層if (instance == null)這裡,這裡讀取到的instance已經不為null了,所以就直接把這個中間狀態的instance拿去用了,就會產生問題。

這裡的關鍵在於——執行緒t1對instance的寫操作沒有完成,執行緒t2就執行了讀操作。

對於version3中可能出現的問題(當然這種概率已經非常小了,但畢竟還是有的嘛~),解決方案是:只需要給instance的宣告加上volatile關鍵字即可,version4版本:

// version 4 

public class singleton

public static singleton getinstance() }}

return instance;

}}

volatile關鍵字的乙個作用是禁止指令重排,把instance宣告為volatile之後,對它的寫操作就會有乙個記憶體屏障(什麼是記憶體屏障?),這樣,在它的賦值完成之前,就不用會呼叫讀操作。

注意:volatile阻止的不是singleton = new singleton()這句話內部[1-2-3]的指令重排,而是保證了在乙個寫操作([1-2-3])完成之前,不會呼叫讀操作(if (instance == null))。

設計模式 建立型 單例模式

單例模式在整個軟體開發中還是比較常用的,頻繁使用且過程穩定的方法 全域性變數都可以使用該模式,也可以叫做公共類。單例模式需要遵循要麼出現一次,要麼不出現的規則。單例模式不提供外部例項化功能,在內部自已例項化以保證其唯一例項。具體如下 class common public static common...

設計模式 建立型 單例模式

單例模式 singleton 保證乙個類僅有乙個例項,並提供乙個訪問它的全域性訪問點。單例模式劃分 class singleton 獲取本類例項的唯一全域性訪問點 public static singleton getinstance return instance 物件屬於引用資料型別,和基本資料...

設計模式 建立型 單例模式

英文singleton,又稱單件模式。描述 確保類只有乙個例項,並且提供了乙個全域性訪問點。在應用的某些場景,我們只需要類的乙個例項就夠了,並且我們需要在應用的多個地方 客戶 方便的獲取該例項物件。比如應用中的乙個浮動工具欄,或者是乙個資訊收集器 專門收集應用中的操作資訊 等等。優點 方便的控制僅唯...