前言
說到物件導向的設計模式,現在很多人都可以隨便說出好幾種常用的,但是有沒有想過設計模式,即使是初學者也至少能說一下SingleTon和Factory Method這兩個。
那麼,設計模式是不是隨便怎麼用都沒問題哪?
這個問題從提問的方式上就可以看出,答案一定是否定的(大家也不是白白接受了這麼多年的應試教育的)。
但是,就我個人的觀察,濫用設計模式的絕對不是少數。而且越是簡單的模式越會被濫用。
從最簡單的模式——SingleTon開始
說到SingleTon,我相信只要知道設計模式的,就知道SingleTon,也寫過SingleTon,可謂是盡人皆知的設計模式了。
就是這個盡人皆知的設計模式,卻是被濫用的最厲害的設計模式,本篇就討論一下關於SingleTon的濫用問題。
安全執行緒是個問題
首先GoF是站在一個純OO的領域思考問題的,所以,很多其他領域的問題並沒有考慮進來(事實上也不適合拉進來一起講),然而實際編程者卻不得不面對更多領域的問題(最常見的是並發領域)。
這也就是為什麼在GoF的SingleTon是如此的簡單,而在Java或.net實際寫SingleTon時,卻需要注意鎖的問題的根本原因。
關於SingleTon是否是安全執行緒的,我傾向於把問題分解成兩個部分:
- SingleTon本身是否安全執行緒
- SingleTon的執行個體是否安全執行緒
關於第一個,通過著名的Java下雙檢鎖不安全問題,相信大家都已經十分清楚了,如果還有不清楚的,請查閱相關資料,本文就不再重複了。
在這裡,要重點說的是第二個問題,SingleTon的執行個體是否安全執行緒。
如果仔細看msdn的話,在大多數類上,msdn都寫了這麼一句話:
Public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.
在msdn中文版中是:
此類型的公用靜態(在 Visual Basic 中為 Shared)成員是安全執行緒的。但不能保證任何執行個體成員是安全執行緒的。
在ms給出的類庫,把靜態成員都寫成安全執行緒的,而對待大部分的執行個體成員卻是放任其線程的不安全,而要求開發人員在開發時處理執行個體成員的線程不安全問題(我相信Java的類庫也是類似的做法)。
那麼在多線程程式中使用SingleTon,會引出什麼問題哪?
首先,SingleTon僅僅允許一個類只有一個執行個體。那麼這裡簡單的做個推理:
- 在這個程式裡面無論何時,當你需要這個類的執行個體的時候,都只能使用這個唯一的執行個體
- 無論在這個程式的哪個線程中,當你需要這個類的執行個體的時候,都只能使用這個唯一的執行個體
- 無論在這個程式中出現什麼樣的並發,當你需要這個類的執行個體的時候,都只能使用這個唯一的執行個體
- 無論在這個程式中出現什麼樣的並發,當你需要調用這個類的執行個體的某個成員時,都只能使用這個唯一的執行個體的成員
- 無論在這個程式中出現什麼樣的並發,為了保證程式是安全執行緒的,當你需要調用這個類的執行個體的某個成員時,都只能使用這個唯一的執行個體的成員,並且需要保證其安全執行緒
- 無論在這個程式中出現什麼樣的並發,為了保證程式是安全執行緒的,當你需要調用這個類的執行個體的某個成員時,如果這個成員不是安全執行緒的,那麼在使用這個唯一的執行個體的成員時都需要正確的線程同步
發現問題了沒有,被SingleTon的執行個體成員的執行緒安全性需要誰來保證?
- 選項A:SingleTon的執行個體保證所有的成員是安全執行緒的
- 選項B:SingleTon的執行個體不保證安全執行緒的,請大家在使用的時候都加上鎖
如果選擇A,那麼,請檢查那些被SingleTon的代碼,看看有沒有用到堆,檢查對堆的任何調用是否都是安全執行緒的,或者已經同步的
如果選擇B,那麼,請檢查所有使用SingleTon的代碼,看看是否都經過了可靠的線程同步(例如Lock那個SingleTon的對象),只要有一處不注意,就導致整個是線程不安全的(但是當下這麼多寫網頁的人,有多少人會去關注那些資源是需要線程同步的嗎?)。
有沒有選項C?有,如果是類庫的話,對外宣稱程式是線程不安全的,請在使用前保證安全執行緒(例如COM中著名的STA);如果是應用程式的話就告訴客戶,本程式是線程不安全的,出任何問題都是有可能的,當初合約就沒說要保證安全執行緒(客戶一定會抓狂)。
為什麼要用SingleTon?
不知道大家有沒有想過,當初為什麼要把這個類型用SingleTon來做。
我看到的大多數答案是節省資源和全域狀態,固定演算法的介面適配(不排除還有更好的答案)。
先討論節省資源的問題,首先不new執行個體一定比new執行個體要節省資源(CPU和記憶體),這點不用質疑,但是如果要做到安全執行緒,似乎就要再考量一下了。
如果SingleTon執行個體本身的實現方式就保證了安全執行緒(僅使用堆棧和參數中的對象,對自身引用的對象唯讀),那麼安全執行緒是0代價的。
如果其實現方式涉及Lock等同步,那麼衝突機率是多少,如果衝突機率足夠的高,那麼大多數時間軸程將進入等待狀態,導致大量佔用時間資源和CPU資源。即使衝突機率很低,由於Lock需要同步Cache和記憶體,所以一樣需要花費一些額外的代價(同步Cache的時間代價)。
說到這裡,還覺得有Lock的SingleTon一定比new執行個體節省資源?未必吧,到底誰省資源還是要具體問題具體分析一下,不Profiling一下,誰又知道結果哪?
至於全域狀態,可以的話,要盡量避免全域狀態的使用,如果必須要使用的話,確實沒什麼好的方案,不過,建議作為全域狀態使用的SingleTon需要保證所有成員的安全執行緒(否則,一個team member的小錯誤,就可能導致全域狀態出錯)。
第三種固定演算法的介面適配,這是我比較提倡的SingleTon用法。這裡涉及幾個部分的要求:
- 演算法固定
- 演算法不存在狀態,所有的變化均來自參數,且實現無需線程同步就可以保證安全執行緒
- 接收方要求符合某介面(泛指)
如果第一點不滿足,那就不可能作為SingleTon存在。第二點是確保使用SingleTon的效能優勢。而第三點是OO立場上的SingleTon必要性,否則為什麼不用靜態方法。
也許這3點比較抽象,舉個實際點的例子:
public class PersonNameComparer : IComparer<Person>{ public int Compare(Person x, Person y) { if (x == y) return 0; if (x == null) return -1; if (y == null) return 1; return string.Compare(x.name, y.name); }}
第一,演算法固定,就是比較2個人的名字,沒有第二種演算法,要是有那也是其它類的職責;第二,不存在狀態,只用了x和y兩個參數,沒有線程同步;第三,因為需要在排序的場合需要IComparer<Person>介面的執行個體,因此不能使用靜態方法(假設不能使用委託)。
因此,這個類型如果是SingleTon的話,那將是合理的(當然這裡不用SingleTon也沒問題)。
後話
N年前,我去面試某公司時,某面試官問我資料庫聯結能不能做SingleTon,我說不能,會引入很多問題,結果面試官很不滿意。
N月前,面試某人,在談到設計模式用在什麼地方時候,舉例說在Web項目中把一個WebService的代理類做成了SingleTon,結果我很不滿意。
哎,SingleTon啊SingleTon,真是GoF引入OO的最大的坑。