大叔手記(10):別再讓面試官問你單例(暨6種實現方式讓你堵住面試官的嘴)
2011-12-19 09:26 湯姆大叔 閱讀(32603) 評論(66) 收藏 舉報引子
經常從Recruiter那里得到抱怨:“湯姆,為什么面試者每次回去的時候都感覺良好,而你卻說此人達不到Senior級別?”
我都是微笑著說:“感覺不一定都是對的哦。”
Recruiter:“那你就不能問點別的么?為什么每次面試者都說你問的是單例?”
我只能解釋:“單例挺好的,可以問出很多基礎知識哦?!?/p>
Recruiter:“大叔,單例我都懂了,不就是程序運行的時候只能有一個實例么?我打電話招人的時候經常都幫你問過了呢!做開發的沒幾個不懂!”
我Faint。。。
為避免引起誤會加注:問這個題目的目的不是僅僅為了單例,而是考察相關的基礎知識,比如靜態構造函數,私有構造函數,鎖,延時創建對象, readonly/const等區別,不會僅以單例論英雄,之所以面試者以為感覺良好,主要是給出其中1-2個單例實現以后就覺得通過了面試,其實沒有察覺到面試官所要考察的真正內容。
本文目的
寫文本的目的,不是說從單例有多重要,多牛逼啥的。其實更多地是建議博客園的兄弟在面試的時候以另外一個角度來看到面試官的問題,做到主動出擊,也就是說當人問你一個問題的時候,絕對不要想著他問的只是問題的表面,可能還隱藏著很多陷阱(因為面試官通常不會有太多時間面試,一般第一次約見都是60-90分鐘,所以不太可能問太多問題,只能問幾個問題,然后再根據這些問題延時出各種問題),所以在你回答問題的時候,盡量要避開這些陷阱,比如單例里我們經常談到加鎖和線程的問題,如果你對多線程不熟悉,防止陷在里面,那可以趕緊主動說出雙鎖這種實現方式,然后回頭一轉話題說:”其實單例要考察我們的東西有很多,比如私有構造函數,靜態構造函數,靜態字段,readonly和const的區別等等“,其實一般說了這么多以后,面試官基本上不會再在單例上揪住不放了,可能只是象征性問一下構造函數的區別而已,因為這時候他已經知道你基本上了解相關的內容了。當然,如果你想欲擒故縱,就是想讓面試官在這個問題上再多問你半小時了,那也可以牽著面試官的鼻子走哦,不過,這個我想一般不太可能吧。
注:這周如果有時間,我會將我去年的一次被面試經歷寫下來與大家分享的(被印度佬面了將近7個小時,其實就是一道字符串的題目以及延伸,再加一些閑聊)。
下面就列舉一下,在面試過程中得到的不同單例版本吧,大家也可以參考一下:
版本1:Recruiter都懂
using System;
public sealed class Singleton
{
private static Singleton instance;
private Singleton() {}
public static Singleton Instance
{
get
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}
這個版本的主要問題,就是線程安全的問題,當2個請求同時方式這個類的實例的時候,可以會在同一時間點上都創建一個實例,雖然一般不會出異常錯誤,但是起碼不是我們談論的只保證一個實例了。
版本2
public sealed class Singleton
{
// 在靜態私有字段上聲明單例
private static readonly Singleton instance = new Singleton();
// 私有構造函數,確保用戶在外部不能實例化新的實例
private Singleton(){}
// 只讀屬性返回靜態字段
public static Singleton Instance
{
get
{
return instance;
}
}
}
標記類為sealed是好的,可以防止被集成,然后在子類實例化,使用在靜態私有字段上通過new的形式,來保證在該類第一次被調用的時候創建實例,是不錯的方式,但有一點需要注意的是,C#其實并不保證實例創建的時機,因為C#規范只是在IL里標記該靜態字段是BeforeFieldInit,也就是說靜態字段可能在第一次被使用的時候創建,也可能你沒使用了,它也幫你創建了,也就是周期更早,我們不能確定到底是什么創建的實例。
版本3
public sealed class Singleton
{
// 依然是靜態自動hold實例
private static volatile Singleton instance = null;
// Lock對象,線程安全所用
private static object syncRoot = new Object();
private Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
}
使用volatile來修飾,是個不錯的注意,確保instance在被訪問之前被賦值實例,一般情況都是用這種方式來實現單例。
版本4
public class Singleton
{
// 因為下面聲明了靜態構造函數,所以在第一次訪問該類之前,new Singleton()語句不會執行
private static readonly Singleton _instance = new Singleton();
public static Singleton Instance
{
get { return _instance; }
}
private Singleton()
{
}
// 聲明靜態構造函數就是為了刪除IL里的BeforeFieldInit標記
// 以去北歐靜態自動在使用之前被初始化
static Singleton()
{
}
}
這種方式,其實是很不錯的,因為他確實保證了是個延遲初始化的單例(通過加靜態構造函數),但是該靜態構造函數里沒有東西哦,所以能有時候會引起誤解,尤其是在code review或者代碼優化的時候,不熟悉的人可能直接幫你刪除了這段代碼,那就又回到了版本2了哦,所以還是需要注意的,不過如果你在這個時機正好有代碼需要執行的話,那也不錯。
版本5
public sealed class Singleton
{
private Singleton()
{
}
public static Singleton Instance { get { return Nested._instance; } }
private class Nested
{
static Nested()
{
}
internal static readonly Singleton _instance = new Singleton();
}
}
這其實是根據版本4的一個變異版本,就不多說了
版本6
public class Singleton
{
// 因為構造函數是私有的,所以需要使用lambda
private static readonly Lazy<Singleton> _instance = new Lazy<Singleton>(() => new Singleton());
// new Lazy<Singleton>(() => new Singleton(), LazyThreadSafetyMode.ExecutionAndPublication);
private Singleton()
{
}
public static Singleton Instance
{
get
{
return _instance.Value;
}
}
}
其實,一般Lazy的默認構造器只能調用傳入泛型類型T的public構造函數的,但是在本例,因為代碼是在我們的Singleton內部,所以調用私有的構造函數是沒問題的,大家可能還有一個疑慮就是這種方式除了能做到Lazy初始化,做到線程安全么?大家看一下上面一層被注釋掉的代碼,多了一個LazyThreadSafetyMode.ExecutionAndPublication參數,意思是設置線程安全,但由于Lazy<T>默認的設置就是線程安全,所以不設置也是有效的。
所以說,面試的時候,如果能夠把上述6個版本的各種實現原理概況說2分鐘的話,我估計面試官一般情況不會再在單例上揪住不放了,而且如果你說的基本上都沒問題的話,這個時候面試官通常已經開始對你有好感了,起碼我是這樣的,因為起碼你已經將C#的一些基礎知識在這個單例問題上體現了不少,不是么?
延伸
當然,如果你這個時候,回答不出那么多的話,我也不會在這個問題上糾纏那么多,免得制造緊張的氣氛影響后面的討論,通常情況下我會及時地切換到另外一個話題上(比如,你簡歷上寫的最強的Skill以便讓你重新恢復信心),如果你問答的都不錯的話,其實我還想再問一小點,就一小點:如何實現泛型版本的單例?
這個可是加分項哦,之前,有人給了一個如下的代碼:
public class Singleton<T> where T : new()
{
private static readonly Lazy<T> _instance
= new Lazy<T>(() => new T());
public static T Instance
{
get { return _instance.Value; }
}
}
曾經這個版本,我在給自己問這個問題的時候,也考慮過,但其實它是有問題的,就是那個new T(),這樣用的話,就默認了T有public的構造函數了,對吧?也就是說,T是有可能存在多個實例的,因為壓根就不滿足我們的先決條件:那就是你的類不能在外部實例化。
我們來帖一段老外給出的代碼:
public abstract class Singleton
{
private static readonly Lazy<T> _instance
= new Lazy<T>(() =>
{
var ctors = typeof(T).GetConstructors(
BindingFlags.Instance
| BindingFlags.NonPublic
| BindingFlags.Public);
if (ctors.Count() != 1)
throw new InvalidOperationException(String.Format("Type {0} must have exactly one constructor.", typeof(T)));
var ctor = ctors.SingleOrDefault(c => c.GetParameters().Count() == 0 && c.IsPrivate);
if (ctor == null)
throw new InvalidOperationException(String.Format("The constructor for {0} must be private and take no parameters.", typeof(T)));
return (T)ctor.Invoke(null);
});
public static T Instance
{
get { return _instance.Value; }
}
}
從上到下,我們來看看是如何實現的:
- 聲明抽象類,以便不能直接使用,必須繼承該類才能用
- 使用Lazy<T>作為_instance,T就是我們要實現單例的繼承類
- Lazy類的構造函數有一個參數(Func類型),也就是和我們的版本6一樣
- 根據微軟的文檔和單例特性,單例類的構造函數必須是私有的,所以這里要加相應的驗證
- 一旦驗證通過,就invoke這個私有的無參構造函數,不用擔心他的效率,因為他只執行一次!
- Instance屬性返回唯一的一個T的實例
我們來實現一個單例類:
class MySingleton : Singleton<MySingleton>
{
int _counter;
public int Counter
{
get { return _counter; }
}
private MySingleton()
{
_counter = 0;
}
public void IncrementCounter()
{
++_counter;
}
}
這個例子,在一般情況下沒問題,但是并行計算的時候還是有問題的,因為上述的泛型版本的代碼使用的Lazy<T>能確保我們在創建單例實例的時候是線程安全的,但是不意味著單例本身是線程安全的,我們來做個例子看看:
static void Main(string[] args)
{
Parallel.For(0, 100, i =>
{
for (int j = 0; j < 1000; ++j)
MySingleton.Instance.IncrementCounter();
});
Console.WriteLine("Counter={0}.", MySingleton.Instance.Counter);
Console.ReadLine();
}
通常Counter的結果是小于100000的,因為單例里的IncrementCounter方法的代碼本身不在線程安全的保護之內,所以如果我們想得到準確的100000這個數字的話,我們需要改一下MySingleton的IncrementCounter方法代碼:
public void IncrementCounter()
{
Interlocked.Increment(ref _counter);
}
這樣,結果就完美了,至此關于單例的問題就差不多就到這兒了,我們來總結一下單例的優缺點吧。
總結
單例的優點:
1.保證了所有的對象訪問的都是同一個實例
2.由于類是由自己類控制實例化的,所以有相應的伸縮性
單例的缺點:
1.額外的系統開銷,因為每次使用類的實例的時候,都要檢查實例是否存在,可以通過靜態實例該解決。
2.無法銷毀對象,單例模式的特性決定了只有他自己才能銷毀對象實例,但是一般情況下我們都沒做這個事情。
同步與推薦
本文已同步至目錄索引:《大叔手記全集》
大叔手記:旨在記錄日常工作中的各種小技巧與資料(包括但不限于技術),如對你有用,請推薦一把,給大叔寫作的動力。
浙公網安備 33010602011771號