【華爲雲技術分享】【極客思考】設計模式:你確定你真的理解了單例模式嗎?

什麼是單例模式?

說到單例模式,其實大家應該都不陌生,因爲真的太常用了,應該所有開發者接觸設計模式的第一個模式。那我這裏一句話簡單說下爲何使用單例:如果你希望你的某個類只需要有一個實例對象,並且全局共享,那麼你就使用單例。

我喜歡的單例模式實現

單例模式常見的實現有懶漢式、餓漢式這兩種方式,但是在這裏,我不想討論這兩種方式,因爲常見所以沒有討論和需要思考的價值。

讓我們來看看以下的幾種方式的一些實現機制:

一、雙重校驗鎖(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對象,區別與我們常用的加鎖的方式而是用空間換時間,給每個線程分配了獨自的變量副本,從而隔離了多線程訪問對數據訪問的衝突,保證了線程安全性。至於這個類和這個機制,這裏就不展開談了,談多了這篇文章就裝不下了。

點擊這裏,瞭解更多精彩內容

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