柚子快報邀請碼778899分享:多線程——單例模式
柚子快報邀請碼778899分享:多線程——單例模式
目錄
·前言
一、設計模式
二、餓漢模式
三、懶漢模式
1.單線程版
2.多線程版
·結尾
·前言
? ? ? ? 前面的幾篇文章中介紹了多線程編程的基礎知識,在本篇文章開始,就會利用前面的多線程編程知識來編寫一些代碼案例,從而使大家可以更好的理解運用多線程來編寫程序,本篇文章會用多線程來實現(xiàn)設計模式中的“單例模式”,這里實現(xiàn)“單例模式”的方式主要介紹兩種:“餓漢模式”和“懶漢模式”,下面進行本篇文章的重點內(nèi)容吧。
一、設計模式
? ? ? ? 本篇文章介紹的單例模式屬于設計模式中的一種,那么什么是設計模式呢?設計模式和象棋中的“棋譜“”比較類似,比如“紅方當頭炮,黑方馬來跳”,針對紅方的一些走法,黑方應招也有一些固定的套路,按照這種套路來下,局勢就不會吃虧,按照棋譜下棋,下出來的棋不會太差,因為棋譜會兜住我們下棋的下限,設計模式也是如此,按照設計模式來寫代碼同樣可以兜住我們的下限。
? ? ? ? 單例模式,是設計模式的一種,它可以保證某個類在程序中只存在唯一的一份實例,而不會創(chuàng)建出多個實例,這點需求在很多場景都需要,比如在我們前面?MySql?篇章?JDBC?編程中的?DataSource?實例就只需要一個。使用單例模式,就可以對我們的代碼進行一個更嚴格的校驗和檢查,不會像口頭約定那樣還可以創(chuàng)建多個實例。
? ? ? ? 單例模式的具體實現(xiàn)有很多種,本篇文章就來介紹兩種實現(xiàn)方式:“餓漢模式”和“懶漢模式”。
二、餓漢模式
? ? ? ? 餓漢模式下實現(xiàn)的單例模式,在類加載時就會創(chuàng)建好對象實例,具體的代碼已經(jīng)運行示例如下所示,通過代碼中的注釋對代碼再進一步介紹:
// 希望這個類在進程中只有一個實例
class Singleton{
private static Singleton instance = new Singleton();
// get 方法設為靜態(tài)方法,這樣其他代碼想要使用這個類的實例就需要通過這個方法來獲取
// 不應該在其他代碼中重新 new 這個對象,而是使用這個方法獲取現(xiàn)成的對象
public static Singleton getInstance() {
return instance;
}
// 將構造方法設為 private 這樣其他代碼中就無法通過構造方法再進行實例化一個新對象
private Singleton() {}
}
public class ThreadDemo1 {
public static void main(String[] args) {
// 利用"餓漢模式"實現(xiàn)的單例模式創(chuàng)建兩個對象,觀察這兩個對象是否相同
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1==s2);
}
}
? ? ? ? 上述的代碼就是“餓漢模式”單例模式中一種簡單的實現(xiàn)方式,這里實例是在類加載的時候就創(chuàng)建了,創(chuàng)建的時機非常早,這就相當于程序一啟動,實例就創(chuàng)建好了,就使用“餓漢”來形容“創(chuàng)建實例非常迫切,非常早”。
三、懶漢模式
? ? ? ? 懶漢模式下實現(xiàn)的單例模式,在類加載的時候不創(chuàng)建實例,在第一次使用的時候才創(chuàng)建實例。這樣的設計方式可以節(jié)省一些不必要的開銷,以生活中的肯德基瘋狂星期四為例,只有在星期四時,肯德基的點餐小程序上才會出現(xiàn)瘋狂星期四的特價餐品,此時使用懶漢模式,不是星期四時就不會加載瘋狂星期四的特價餐品,就會節(jié)省一些開銷。
1.單線程版
? ? ? ? 下面來以懶漢模式來實現(xiàn)一個單線程版的單例模式,示例代碼及運行結果如下所示:
// 懶漢模式---單線程版
class SingletonLazy{
// 這個引用指向唯一實例,初始化為 null,而不是立即創(chuàng)建實例
private static SingletonLazy instance = null;
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if (instance == null){
// 首次調(diào)用 getInstance 方法,創(chuàng)建實例
instance = new SingletonLazy();
}
// 如果不是第一次調(diào)用 getInstance 方法,直接返回之前創(chuàng)建好的實例
return instance;
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
// 利用"懶漢模式"實現(xiàn)的單例模式創(chuàng)建兩個對象,觀察這兩個對象是否相同
SingletonLazy b = SingletonLazy.getInstance();
SingletonLazy b2 = SingletonLazy.getInstance();
System.out.println(b==b2);
}
}
? ? ? ? 由運行結果可以看出,上述的代碼寫法仍然可以保證該類的實例是唯一一個,與此同時,創(chuàng)建實例的時機就不是程序啟動時了,而是第一次調(diào)用?getInstance?方法的時候。?
2.多線程版
? ? ? ? 通過上面單線程版的懶漢模式實現(xiàn)單例模式,我們可以來分析一下上述的代碼是否是線程安全的呢?結論一定是不安全的,不然也不會再創(chuàng)建一個多線程版的懶漢模式實現(xiàn)單例模式,那么以上代碼在哪里會涉及到線程安全問題呢?這里出現(xiàn)問題的核心代碼就是?getInstance?方法,下面通過畫圖的方式來對這里的線程安全問題進行講解:
? ? ? ? 如上圖所示,在線程 t1?判斷完成,當前是第一次執(zhí)行?getInstance?方法后進入?if?語句內(nèi),沒等創(chuàng)建實例就被調(diào)度走去執(zhí)行線程 t2 ,此時 t2?雖然是第二次調(diào)用?getInstance?方法,但是由于線程 t1?調(diào)用?getInstance?方法還沒有創(chuàng)建實例,所以線程 t2?執(zhí)行?if?語句顯示?instance?仍然為?null,此時線程 t2?開始創(chuàng)建實例,并返回實例,然后又跳轉(zhuǎn)回線程 t1 ,t1?繼續(xù)執(zhí)行創(chuàng)建實例,這時,該進程中就會出現(xiàn)兩個實例,也就出現(xiàn)了線程安全問題。??
? ? ? ? 如何改進單線程的懶漢模式,使它也成為線程安全的代碼呢?這就需要我們進行加鎖操作,想要使這里的代碼執(zhí)行正確,其實只需把?if?和?創(chuàng)建實例的兩個操作打包成原子的(不可拆分),這樣就可以解決單線程的懶漢模式中的線程安全的問題,加鎖邏輯如下圖所示:
? ? ? ? 如上圖兩個線程在加鎖后的執(zhí)行流程所示,此時就可以確保,一定是 t1?執(zhí)行完實例(new)操作修改了?instance?之后再回到 t2?執(zhí)行?if?語句了,這時?if 的條件就不會成立了,t2?就會直接返回?instance 了。
? ? ? ? 但是這樣加鎖之后還有一個問題,如果?instance?已經(jīng)創(chuàng)建過實例了,此時后續(xù)再調(diào)用?getInstance?方法就都是直接返回?instance?實例了,這時調(diào)用?getInstance?方法就屬于純粹的讀操作了,就不會有線程安全問題了,不過,按照上圖中的代碼邏輯,即使創(chuàng)建完?instance?實例后是線程安全的代碼,仍然每次調(diào)用都會先加鎖再釋放鎖,此時效率就會變低(加鎖意味著產(chǎn)生阻塞,一旦阻塞解除時間就不確定了)。
? ? ? ? 為了解決上述加鎖引入的新問題,我們可以在每次加鎖前再進行一次判斷,仍然是判斷當前?instance 的值是否為?null ,為?null 就繼續(xù)加鎖,不為?null?就可以直接返回?instance?對象,不用再進行加鎖操作了,具體代碼如下圖所示:
? ? ? ? 如上圖所示的代碼中,synchronized?上下兩條?if?語句中判斷的內(nèi)容是一樣的,這里雖然?if?中進行的判斷相同,但是所判斷的含義還是有所差別:
第一個?if?判斷當前是否要加鎖;第二個?if?判斷的是當前是否要創(chuàng)建實例?
? ? ? ? 上面代碼很湊巧的?if?中的判斷條件相同了,但是一個是為了保證“線程安全”一個是“保證“執(zhí)行效率”,這也就形成了雙重校驗鎖。
? ? ? ? 代碼改到此處,還是存在一個問題,那就是由指令重排序引起的線程安全問題,指令重排序是一種編譯器的優(yōu)化方式,調(diào)整原有的代碼執(zhí)行順序,保證邏輯不變的前提下提高程序的效率,但是在多線程中,這種優(yōu)化就很可能帶來線程安全問題,上面代碼中,創(chuàng)建?instance?實例的過程就很可能會被指令重排序,創(chuàng)建?instance?實例代碼如下:
instance = new SingletonLazy();
? ? ? ? 上面這段代碼,可以拆分成三個大的步驟:
申請一段內(nèi)存空間;在這個內(nèi)存空間上調(diào)用構造方法,創(chuàng)建出這個實例;把這個內(nèi)存地址賦值給?instance?引用變量。?
? ? ? ? 正常的情況下,會按 1,2,3 的順序來執(zhí)行上面這段代碼,但是編譯器可能會將上面代碼優(yōu)化成 1,3,2 的順序來執(zhí)行,這時就可能會出現(xiàn)問題,如下圖所示的情況:?
? ? ? ? 如上圖的線程調(diào)度過程,t2?線程執(zhí)行完?getInstance?方法后得到的是一個各個屬性都未初始化“全0”值的?instance?實例,此時如果使用 t2?線程如果使用了?instance?里面的屬性或者方法就會出現(xiàn)錯誤。
? ? ? ? 這種錯誤出現(xiàn)的原因是由于線程 t1?在創(chuàng)建實例執(zhí)行完了 1,3?后,被調(diào)度走,此時?instance?指向的是一個非?null 的,但是未初始化的對象,這時 t2?線程就會判定?instance==null?不成立,直接?return ,得到一個各個屬性都未初始“全0”值的?instance?實例,此時使用這個實例就會出現(xiàn)問題,但是如果創(chuàng)建實例的代碼按照 1,2,3 的順序來執(zhí)行,就不會出現(xiàn)上述的問題了,所以解決這個問題的方法就是阻止編譯器對這段代碼的指令重排序,這就需要使用到我們前面文章介紹的關鍵字?volatile 了。
? ? ? ? 這里還是再介紹一下?volatile?關鍵字的功能把,主要有兩個:
保證內(nèi)存可見性:每次訪問變量都必須要重新讀取內(nèi)存,而不會優(yōu)化成到寄存器或緩存中讀取變量;禁止指令重排序:針對這個?volatile?關鍵字修飾的變量的讀寫操作相關指令是不能被重排序的。?
? ? ? ? 代碼中需要進行指令重排序的地方是為?instance?創(chuàng)建實例的時候,所以我們可以直接針對這個變量加上?volatile?關鍵字進行修飾,這樣,針對這個變量再進行讀寫操作就不會出現(xiàn)重排序了,此時,創(chuàng)建實例的順序一定是 1,2,3?也就預防了上述的問題。
? ? ? ? 代碼修改到這里就算沒有問題了,那么正確懶漢模式實現(xiàn)單例模式多線程版的代碼就可以寫出來了,代碼及一些詳細注釋如下所示:
// 懶漢模式---多線程版
class SingletonLazy{
// 這個引用指向唯一實例,初始化為 null,而不是立即創(chuàng)建實例
private volatile static SingletonLazy instance = null;
private static Object locker = new Object();
private SingletonLazy() {}
public static SingletonLazy getInstance() {
// 如果 instance 為 null, 說明是首次調(diào)用,首次調(diào)用就需要考慮線程安全問題,需要加鎖
if (instance == null) {
synchronized (locker) {
if (instance == null){
// 首次調(diào)用 getInstance 方法,創(chuàng)建實例
instance = new SingletonLazy();
}
}
}
// 如果不是第一次調(diào)用 getInstance 方法,直接返回之前創(chuàng)建好的實例
return instance;
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
// 利用"懶漢模式"實現(xiàn)的單例模式創(chuàng)建兩個對象,觀察這兩個對象是否相同
SingletonLazy b = SingletonLazy.getInstance();
SingletonLazy b2 = SingletonLazy.getInstance();
System.out.println(b==b2);
}
}
·結尾
? ? ? ? 文章到這里就要結束了,本篇文章利用前面文章介紹的多線程基礎知識來實現(xiàn)了一個小案例——單例模式的實現(xiàn),這里介紹的兩種實現(xiàn)方式:餓漢模式與懶漢模式,由于餓漢模式從類加載時就已經(jīng)創(chuàng)建好實例,后續(xù)獲取實例都是讀操作不涉及線程安全問題,所以餓漢模式下的單例模式代碼天生就是線程安全的,反觀,懶漢模式在多線程與單線程下就有很大的差別了,此時單線程版的懶漢模式在多線程中就會引發(fā)線程安全問題,上面文章詳細介紹了每個會出現(xiàn)線程安全問題的地方,希望能夠給大家講解清楚,最后在基于單線程版的懶漢模式代碼下,修改出了多線程版的懶漢模式代碼,理解清楚這里相信會對你理解線程安全問題有很大的幫助,如果對文章哪里感到疑惑,歡迎在評論區(qū)進行留言討論哦~我們下一篇文章再見~~~
柚子快報邀請碼778899分享:多線程——單例模式
好文鏈接
本文內(nèi)容根據(jù)網(wǎng)絡資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉(zhuǎn)載請注明,如有侵權,聯(lián)系刪除。