重學設計模式-單例模式

渣男小四發表於2021-05-24

願你生命中有夠多的雲翳,造就一個美好的黃昏 

   

介紹

  單例模式是指一個類在整個程式執行中只允許存在一個例項,也就是說在JVM裡面只存在一個例項,單例模式應用十分廣泛,比如說一個公司裡面只有一個CEO,一個家庭裡面只有一個爸爸(當然,排除那些意外),單例模式主要應用在需要頻繁使用建立和使用的一些類上面,因為只存在一個例項,所以節省了記憶體的開銷,所有執行緒共享同一個例項,試想一下,如果一個類使用十分頻繁,沒有使用單例模式的情況下,一個執行緒需要建立一個例項,那麼系統中將會出現出現很多多餘的例項,對記憶體的消耗也很大,JVM中容易發生GC,比如資料庫連線池,某些不太常用的物件,皆可使用單例模式來做,有助於提高系統的可用性。

  

單例模式的種類

  單例模式有很多種寫法,大致可分為執行緒安全和執行緒不安全,下面我們來看一下具體的種類以及實現方式。

  一.餓漢式單例模式

  餓漢式顧名思義就是很餓,想要馬上得到,正如一個飢渴已久的漢子一樣,想要馬上得到愛情的滋養,換到程式裡面來也一樣,一個物件在程式啟動時就進行初始化,建立一個單例物件,也就是說程式已啟動,他馬上進行例項化,JVM就存在一個單例物件,後面就不會再建立,它是執行緒安全的,因為後面的執行緒來訪問的時候,例項已經存在,直接使用就行,不用再去例項化,所以它的效率非常高,但是凡事有利有弊,我們已經看出,再系統啟動時就進行就對物件進行初始化,那麼如果這個例項使用並不是很多,那會造成記憶體的浪費,如果系統中存在大量的單例物件,那麼使用餓漢式可能就不是那麼合理啦,下面來具體實現一下餓漢式。

  

   我們看執行結果,開啟多個執行緒,不管怎麼執行,獲取到的都是同一個例項,因為在程式啟動時就已經做了初始化操作,JVM已經存在這個例項,所以使用的就是這個例項。

    

  

 

  二.懶漢式單例模式

  懶漢式顧名思義就是很懶,現實生活中的懶漢都是火燒到自家門前才著急,在我們農村老人有這樣一句話,屎要拉出來了才想到挖茅坑,這樣的比喻應該恰當吧,換到程式裡面來,有很多例子,比如前端的樹結構懶載入,不會一下子將所有子節點都遍歷出來,而是點選那一層,再載入出下級,懶漢式就是運用這樣的思想,需要用的時候我再載入,這樣的好處是節省記憶體空間,如果一個物件不經常用,我們就不需要在程式初始化時就將其載入,但是它會出現執行緒安全問題,具體看程式碼。

  

   如圖可知,給物件一個初始值為空,如果執行緒訪問的時候,判斷到物件是空的,則進行例項化,當第二個執行緒訪問的時候,因為第一個執行緒已經進行例項化,所以直接返回,而不用再進行例項化,那麼會有一種情況,如果兩個或者兩個以上的執行緒同時訪問呢,當多個執行緒跑到判空條件那裡時,當第一個執行緒還沒有完成建立物件,第二個執行緒判斷到物件依然為空,所以進行建立,這樣,就會建立兩個物件,所以是執行緒不安全的,我們看下結果。

  

   執行多次以後,我們發現建立了兩個物件,那麼如何避免這種情況呢,當然有方法,我們可以使用加鎖的方式來實現執行緒安全問題,如下。

  

   我們使用了同步阻塞鎖synchronized鎖來同步方法,這樣,每個執行緒訪問時都要等待當前執行緒訪問完成後才能進行訪問,那麼其中就有一個問題,明明只需要一個執行緒建立物件後其他執行緒就能使用,而現在,明明物件已經建立完成了,只需要判斷一下就返回物件,但是我一堆執行緒還阻塞在外面,只是為了一個判斷,這是很不合理的,沒事,我們繼續改進。

  

  三.雙重校驗鎖(DCL

  懶漢式加鎖可以實現執行緒安全,但是其效率很低,所以我們使用了雙重鎖校驗,程式碼如下。

  

   我們從上面程式碼中可以看出,使用了兩次if判斷,我們解讀一下,如果此時物件還沒有進行例項化,還為null,此時兩個執行緒同時訪問,兩個執行緒同時進入了第一個if語句,但是到了同步程式碼塊這裡的時候第一個執行緒進入了同步程式碼塊,第二個執行緒被阻塞了,當第一個執行緒例項化完成以後,第二個執行緒進入同步程式碼塊以後判斷instance不為空,則直接跳出判斷,返回例項,後續的執行緒通過第一個判斷,就直接返回,這樣效率就會變得很高,也不存線上程安全問題。

 

  四.靜態內部類實現

  上面我們使用了雙重做校驗方式實現了單例模式,線上程安全和效率上面都很不多,但是我們依然使用同步鎖,那麼還有沒有更加優雅,效率更高的寫法呢,答案是肯定有的,我們可以利用java的一些特性來實現,這裡就使用了靜態內部類,我們知道,java的內部類會在外部類執行前先執行,這樣我們就可以在內部類裡面進行例項化,然後外部類獲取例項,程式碼如下。

  

   因為在初始化的時候靜態內部類已經完成物件的例項化,所以不存線上程安全問題。

 

  五.使用列舉

  這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多執行緒同步問題,而且還自動支援序列化機制,防止反序列化重新建立新的物件,絕對防止多次例項化。不過,由於 JDK1.5 之後才加入 enum 特性,這種方式能夠防止反射攻擊(上面幾種都可以通過反射來獲取類的私有建構函式,從而能夠建立多個例項),而enum可以防止。

  

   

   

 

相關文章