願你生命中有夠多的雲翳,造就一個美好的黃昏
介紹
單例模式是指一個類在整個程序運行中只允許存在一個實例,也就是說在JVM里面只存在一個實例,單例模式應用十分廣泛,比如說一個公司里面只有一個CEO,一個家庭里面只有一個爸爸(當然,排除那些意外),單例模式主要應用在需要頻繁使用創建和使用的一些類上面,因為只存在一個實例,所以節省了內存的開銷,所有線程共享同一個實例,試想一下,如果一個類使用十分頻繁,沒有使用單例模式的情況下,一個線程需要創建一個實例,那么系統中將會出現出現很多多余的實例,對內存的消耗也很大,JVM中容易發生GC,比如數據庫連接池,某些不太常用的對象,皆可使用單例模式來做,有助於提高系統的可用性。
單例模式的種類
單例模式有很多種寫法,大致可分為線程安全和線程不安全,下面我們來看一下具體的種類以及實現方式。
一.餓漢式單例模式
餓漢式顧名思義就是很餓,想要馬上得到,正如一個飢渴已久的漢子一樣,想要馬上得到愛情的滋養,換到程序里面來也一樣,一個對象在程序啟動時就進行初始化,創建一個單例對象,也就是說程序已啟動,他馬上進行實例化,JVM就存在一個單例對象,后面就不會再創建,它是線程安全的,因為后面的線程來訪問的時候,實例已經存在,直接使用就行,不用再去實例化,所以它的效率非常高,但是凡事有利有弊,我們已經看出,再系統啟動時就進行就對對象進行初始化,那么如果這個實例使用並不是很多,那會造成內存的浪費,如果系統中存在大量的單例對象,那么使用餓漢式可能就不是那么合理啦,下面來具體實現一下餓漢式。
我們看執行結果,開啟多個線程,不管怎么執行,獲取到的都是同一個實例,因為在程序啟動時就已經做了初始化操作,JVM已經存在這個實例,所以使用的就是這個實例。
二.懶漢式單例模式
懶漢式顧名思義就是很懶,現實生活中的懶漢都是火燒到自家門前才着急,在我們農村老人有這樣一句話,屎要拉出來了才想到挖茅坑,這樣的比喻應該恰當吧,換到程序里面來,有很多例子,比如前端的樹結構懶加載,不會一下子將所有子節點都遍歷出來,而是點擊那一層,再加載出下級,懶漢式就是運用這樣的思想,需要用的時候我再加載,這樣的好處是節省內存空間,如果一個對象不經常用,我們就不需要在程序初始化時就將其加載,但是它會出現線程安全問題,具體看代碼。
如圖可知,給對象一個初始值為空,如果線程訪問的時候,判斷到對象是空的,則進行實例化,當第二個線程訪問的時候,因為第一個線程已經進行實例化,所以直接返回,而不用再進行實例化,那么會有一種情況,如果兩個或者兩個以上的線程同時訪問呢,當多個線程跑到判空條件那里時,當第一個線程還沒有完成創建對象,第二個線程判斷到對象依然為空,所以進行創建,這樣,就會創建兩個對象,所以是線程不安全的,我們看下結果。
運行多次以后,我們發現創建了兩個對象,那么如何避免這種情況呢,當然有方法,我們可以使用加鎖的方式來實現線程安全問題,如下。
我們使用了同步阻塞鎖synchronized鎖來同步方法,這樣,每個線程訪問時都要等待當前線程訪問完成后才能進行訪問,那么其中就有一個問題,明明只需要一個線程創建對象后其他線程就能使用,而現在,明明對象已經創建完成了,只需要判斷一下就返回對象,但是我一堆線程還阻塞在外面,只是為了一個判斷,這是很不合理的,沒事,我們繼續改進。
三.雙重校驗鎖(DCL)
懶漢式加鎖可以實現線程安全,但是其效率很低,所以我們使用了雙重鎖校驗,代碼如下。
我們從上面代碼中可以看出,使用了兩次if判斷,我們解讀一下,如果此時對象還沒有進行實例化,還為null,此時兩個線程同時訪問,兩個線程同時進入了第一個if語句,但是到了同步代碼塊這里的時候第一個線程進入了同步代碼塊,第二個線程被阻塞了,當第一個線程實例化完成以后,第二個線程進入同步代碼塊以后判斷instance不為空,則直接跳出判斷,返回實例,后續的線程通過第一個判斷,就直接返回,這樣效率就會變得很高,也不存在線程安全問題。
四.靜態內部類實現
上面我們使用了雙重做校驗方式實現了單例模式,在線程安全和效率上面都很不多,但是我們依然使用同步鎖,那么還有沒有更加優雅,效率更高的寫法呢,答案是肯定有的,我們可以利用java的一些特性來實現,這里就使用了靜態內部類,我們知道,java的內部類會在外部類執行前先執行,這樣我們就可以在內部類里面進行實例化,然后外部類獲取實例,代碼如下。
因為在初始化的時候靜態內部類已經完成對象的實例化,所以不存在線程安全問題。
五.使用枚舉
這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機制,防止反序列化重新創建新的對象,絕對防止多次實例化。不過,由於 JDK1.5 之后才加入 enum 特性,這種方式能夠防止反射攻擊(上面幾種都可以通過反射來獲取類的私有構造函數,從而能夠創建多個實例),而enum可以防止。