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

摘要:單例模式是建立類型中經常使用的一種設計模式。該模式下的類有且僅有一個實例。

什麼是單例模式?

說到單例模式,其實你們應該都不陌生,由於真的太經常使用了,應該全部開發者接觸設計模式的第一個模式。那我這裏一句話簡單說下爲什麼使用單例:若是你但願你的某個類只須要有一個實例對象,而且全局共享,那麼你就使用單例。java

我喜歡的單例模式實現

單例模式是建立類型中經常使用的一種設計模式。該模式下的類有且僅有一個實例。單例模式常見的實現有懶漢式、餓漢式這兩種方式,可是在這裏,我不想討論這兩種方式,由於常見因此沒有討論和須要思考的價值。web

讓咱們來看看如下的幾種方式的一些實現機制:數據庫

1、雙重校驗鎖(DCL)

上代碼:編程

開發、單例模式、線程、segmentfault

DCL雙重加鎖的方式保證每次調用getSingleton方法的時候都是同步的。其實加鎖你們都能理解,就是解決多線程同步的問題。但其實這裏有個重點,就是這行代碼:設計模式

private volatile static Singleton singletontomcat

爲何要用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了,返回了。

2、靜態內部類

上代碼:

靜態內部類是一個我比較喜歡的實現方式,固然很明顯代碼少,邏輯較爲簡單。這種方式主要是利用了classloader機制來保證初始化singleton的時候只有一個線程,避免了須要再去保證線程同步的問題。同時咱們把這種方式實例化有lazy loading的效果,其實主要是由於靜態內部類Holder類並不會在Singleton類被裝載的時候就被初始化了,只有當Holder類被主動使用,也就是調用了getSingleton方法以後,纔會顯示的裝載Holder類,從而實例化singleton對象。若是singleton對象是一個消耗資源佔用比較大的內存的對象的時候,若是你但願延遲加載的話,那麼這種方式是個不錯的選擇。

可是其實靜態內部類的方式實際上並無想象中的那麼完美,由於它沒法阻擋反射和反序列攻擊,你能夠利用前面兩種方式再去構造新的Singleton的實例,因此不是嚴格意義上的單例。

3、枚舉

上代碼:

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

點擊關注,第一時間瞭解華爲雲新鮮技術~

相關文章
相關標籤/搜索