前言
單例模式,這個最簡單的設計模式,有無數開發者在網絡上寫過樣本,我相信只要混過的,都能閉着眼睛把單例寫出來,並不稀奇。
但是很多人寫單例,都是背着寫出來的,認為寫法是固定的,其實並非如此。
是戴套還是結扎?
很多夫妻都會遇到的問題:怎樣確保只生一個小孩?
我認為,擺在面前的有兩種方式:戴套和結扎。戴套是在外面處理,結扎是在內部處理,都可以達到這個效果,但是哪種更優呢?
接下來我們就探討一下,單例模式所要解決的問題,與解決問題的思路演變。
方案一:調用方處理
原始類:
public class User {
}
我要確保這個User類只實例化一次,我可以這樣子做:
public static User user = null; public static void main(String[] args) { if(null == user){ user = new User(); } User user1 = user; User user2 = user; }
這樣是不是只實例化一次了? 用一個變量來保存User對象,如果變量是Null就實例化User,(也可以采用反射來實現)。
問題:
如果這樣子做的話,程序其他地方也要使用這個對象該怎么辦?
必須使用全局變量來保存該對象,得靠程序之間的約定才能保持單例。
然而,全局變量會鼓勵開發人員,用許多全局變量指向許多小對象造成不必要的引用污染,並且,對於調用方來說,很累,保持單例,到底是你的事情還是我的事情?
顯然,單例模式傾向於內部處理,在外部處理就是普通的程序邏輯處理,並不能稱作模式。
方案二:內部處理
改寫后的User類:
public class User { private static User user = null; private User() { } public static User getInstance(){ if(null == user){ user = new User(); } return user; } }
一般來說,我們使用的單例模式都是這個樣子,私有靜態變量保存實例、私有構造器拒絕構造、公有靜態方法對外提供實例。
但是當多線程的情況下就會出問題了,當線程A執行到user = new User()這行代碼的時候,但還沒獲得對象(對象的初始化是需要時間的),此時線程B執行到if(null == user)判斷,那么線程B的判斷結果也是真,於是兩個線程都進去各自new了一個User對象,內存中就出現了兩個對象。
第一次優化:同步鎖
為了解決多線程場景下單例出現多個實例的問題,我們把getInstance()方法上一個同步鎖:
public synchronized static User getInstance(){ if(null == user){ user = new User(); } return user; }
不管出現多少個線程,全部給我排隊,等上一個線程離開該方法之后,才可進入,這樣即可解決問題。
但是這樣又衍生出了問題,因為嚴格來說,只有第一次實例化這個對象的時候需要線程同步,避免出現多個實例,一旦User對象被實例化出來之后,就不需要對多線程進行同步了,同步一個方法可能造成程序執行效率下降一百倍,每次同步會嚴重拖垮程序性能。
所以同步鎖雖然解決了多線程問題,但是付出了性能作為代價,這並不是最優的方案。
第二次優化:雙重檢查加鎖
既然在方法上加鎖得不償失,那么我就先判斷是否是null,是null之后我再對該對象加鎖:
public class User { private volatile static User user = null; private User() { } public static User getInstance(){ if(null == user){ synchronized (User.class){ if(null == user){ user = new User(); } } } return user; } }
只有第一次實例化的時候user才是null,所以進入代碼塊只有一次,進去之后,再讓所有的線程同步,同步之后再檢查一下user是不是null。
這里再次檢查有什么意義呢?有意義。
請注意user變量的定義多了一個volatile關鍵字,該關鍵字可以讓所有使用該變量的線程都能實時更新變量的最新狀態。
運行流程:線程AB同時進入第一個判斷,然后同步,A先進synchronized,B再外面等,A在里面new了User對象,退出,B再進,此時如果不判斷的話,B也會再次new一個,正因為user變量是用volatile來定義的,所以Anew了對象后,B線程的user對象也會更新到最新值,也就不等於null了。
這次優化完美解決了多線程的問題,但是仍然感覺有問題,問題就是怪怪的。。。。
因為解決的方案並不優雅,兩個相同的判斷,中間再插一根同步鎖,顯得這段代碼對程序員來說有點像修bug填坑,並非是一個漂亮的設計模式。
第三次優化:餓漢式單例
我們從根本上思考:多線程問題主要是由於user對象是Null,多個線程同時去獲取實例才引發的麻煩。
如果user對象從一開始就不是Null呢?
好主意!
立刻改寫代碼:
public class User { private final static User user = new User(); private User() { } public static User getInstance(){ return user; } }
直接在變量初始化的時候就實例化對象,利用這個做法,JVM在加載這個類時就會創建User這個對象,並且特意加了final關鍵字,則user變量的值一旦在初始化之后便不能更改。
這種單例模式也被稱作餓漢式單例,因為程序啟動后,盡管沒有線程來訪問,內存中也已經存在了User對象。
經過兩個方案,三次優化,目前,這種餓漢寫法的單例可以被稱作簡單優雅並無副作用的單例模式。