【極客思考】設計模式:你確定你真的理解了單例模式嗎?

華為雲開發者社群發表於2020-05-27
摘要:單例模式是建立型別中常用的一種設計模式。該模式下的類有且僅有一個例項。

什麼是單例模式?

說到單例模式,其實大家應該都不陌生,因為真的太常用了,應該所有開發者接觸設計模式的第一個模式。那我這裡一句話簡單說下為何使用單例:如果你希望你的某個類只需要有一個例項物件,並且全域性共享,那麼你就使用單例。
我喜歡的單例模式實現
 
單例模式是建立型別中常用的一種設計模式。該模式下的類有且僅有一個例項。單例模式常見的實現有懶漢式、餓漢式這兩種方式,但是在這裡,我不想討論這兩種方式,因為常見所以沒有討論和需要思考的價值。
讓我們來看看以下的幾種方式的一些實現機制:

一、雙重校驗鎖(DCL)

上程式碼:
開發、單例模式、執行緒、
DCL雙重加鎖的方式保證每次呼叫getSingleton方法的時候都是同步的。其實加鎖大家都能理解,就是解決多執行緒同步的問題。但其實這裡有個重點,就是這行程式碼:
private volatile static Singleton singleton
為什麼要用volatile去修飾呢,這邊從兩個方面去說明:
 
1.如果不用volatile修飾會怎麼樣?
 
這看起來似乎也是行的通的,但是瞭解過編譯器和程式指令的話就會知道那是不可靠的,具體原因如下:
  1. 編譯器優化了程式指令,以加快cpu處理速度。
  2. 多核cpu會動態調整表指令順序,以加快並行運算能力。
簡單理解,那就是現在都0202年了,一臺計算機cpu和核心都是好幾個出現的,不在是那個單核的老時代了,所以java檔案編譯成位元組碼指令之後,你的編碼邏輯確實是序列的,計算機也會根據正規化把你程式設計的邏輯結果給你執行返回,但是具體到cpu去執行指令的時候,為了體現多核的優勢,會對一些指令做並行處理,以加快程式執行速度。
 
我想好奇的你還是想知道,如果不加volatile的話,會在什麼時候出現問題,那我給你說說問題出現的順序:
  1. 執行緒A,呼叫方法獲取例項,發現物件未例項化,準備開始例項化。
  2. 由於編譯器優化了程式的指令,允許物件在建構函式未呼叫完成前,將共享變數的引用指向部分構造的物件,雖然物件未完全例項化,但是已經不為null了。
  3. 執行緒B進入也要呼叫方法獲取例項,發現部分構造的物件已經不為null,則直接返回了該物件。
至於執行緒B返回之後會發生什麼,可想而知,沒例項化完,那麼就會導致呼叫部分的方法的時候,就會有空指標的異常,所以就是我上面說的,不可靠。
 
 
2.volatile作用是啥?
 
為了解決這個問題,JDK1.6之後的版本提供了該關鍵字, 其實就是為了讓其修飾的變數你能夠線上程間可見,而所謂的可見,那就是大家都從主存中獲取,至於主存等概念在這裡就不展開說明了。
可以這麼理解:線上程B讀取volatile變數後,執行緒A在寫這個volatile之前,所有可見的共享變數的值都將立即變得對執行緒B可見。
對應上面的問題解決也就是:執行緒A在未初始化完,singleton變數那就是null,執行緒B讀到的也就是null,那麼當執行緒B再進去想要加鎖例項化的時候,發現執行緒A獲取了鎖正在例項化,那就阻塞了起來,直到A例項化完釋放鎖,但是因為例項化完之後B立馬又知道該變數不為null了所以在第二個判斷的時候,就不用進去new了,返回了。

二、靜態內部類

上程式碼:
靜態內部類是一個我比較喜歡的實現方式,當然很明顯程式碼少,邏輯較為簡單。這種方式主要是利用了classloader機制來保證初始化singleton的時候只有一個執行緒,避免了需要再去保證執行緒同步的問題。同時我們把這種方式例項化有lazy loading的效果,其實主要是因為靜態內部類Holder類並不會在Singleton類被裝載的時候就被初始化了,只有當Holder類被主動使用,也就是呼叫了getSingleton方法之後,才會顯示的裝載Holder類,從而例項化singleton物件。如果singleton物件是一個消耗資源佔用比較大的記憶體的物件的時候,如果你希望延遲載入的話,那麼這種方式是個不錯的選擇。
 
但是其實靜態內部類的方式實際上並沒有想象中的那麼完美,因為它無法阻擋反射和反序列攻擊,你可以利用前面兩種方式再去構造新的Singleton的例項,所以不是嚴格意義上的單例。

三、列舉

上程式碼:
這種方式是Josh Bloch提倡的,利用列舉的特性,讓JVM來保證執行緒安全和單例的問題,還能防止反序列化和反射,除了大家不怎麼常用外,其實這種簡單的方式是個很好的方式。
反編譯看一下,其實列舉是在static塊中進行的物件的建立:

單例模式真的有那麼好嗎?

優點:
 
1.提供了唯一例項的受控訪問。
2.因為只有一個例項,節約了系統資源,提高系統效能。
 
缺點:
 
1.單例模式沒有抽象層,擴充套件比較困難。
2.單例類的職責過重,違背了“單一職責原則”。
 
我的推薦
 
我們去使用單例基本目標就是為了節省記憶體資源,而且一般的web專案都會引入Spring框架,通過Spring實現的單例和上面設計模式說的單例有所不同。設計模式的單例是在整個Java應用中只有一個例項,而Spring中的單例是在一個IOC容器中就只有一個單例。但對於web應用來說,web容器(Jetty或tomcat)對使用者的每個請求都會建立一個單獨的servlet執行緒去處理請求,Spring框架下的介面每個action也都是單例的,那麼其實就保證了我們使用的是一個例項。
 
同時Spring也支援我們通過註解或者xml進行lazy-init,也可以指定scope確定其是否為全域性單例,又或者是多個例項,對於程式來說有了更多的選擇。
當然上面提到的執行緒安全的問題,其實大多數情況下Spring是沒有去保證所有bean的執行緒安全,所以主動權交給了開發者,我們自己編寫程式要保證執行緒安全的。不過在我們經常使用的資料庫dao層的那些dao 的bean物件,Spring通過ThreadLocal物件,區別與我們常用的加鎖的方式而是用空間換時間,給每個執行緒分配了獨自的變數副本,從而隔離了多執行緒訪問對資料訪問的衝突,保證了執行緒安全性。至於這個類和這個機制,這裡就不展開談了,談多了這篇文章就裝不下了。
 
 

相關文章