單例模式,是Java中比較常見的一個設計模式,也是我在面試時經常會問到的一個問題。 經過我的初步統計,基本上有60%左右的人可以說出2-4種單例的實現方式,有40%左右的人可以說出5-6種單例的實現方式,只有20%左右的人能夠說出7種單例的實現。 而只有不到1%的人能夠說出7種以上的單例實現。 其實 ...
單例模式,是Java中比較常見的一個設計模式,也是我在面試時經常會問到的一個問題。
經過我的初步統計,基本上有60%左右的人可以說出2-4種單例的實現方式,有40%左右的人可以說出5-6種單例的實現方式,只有20%左右的人能夠說出7種單例的實現。
而只有不到1%的人能夠說出7種以上的單例實現。
其實,作為面試官,我大多數情況下之所以問單例模式,是因為這個題目可以問到很多知識點。
比如線程安全、類載入機制、synchronized的原理、volatile的原理、指令重排與記憶體屏障、枚舉的實現、反射與單例模式、序列化如何破壞單例、CAS、CAS的ABA問題、Threadlocal等知識。
一般情況下,只需要從單例開始問起,大概就可以完成一場面試的整個流程,把我想問的東西都問完,可以比較全面的瞭解一個面試者的水平。
以下,是一次面試現場的還原,從單例模式開始:
Q:你知道怎麼不使用synchronized和lock實現一個線程安全的單例嗎?
A:我知道,可以使用"靜態內部類"實現。
靜態內部類實現單例模式:
Q:除了靜態內部類還會其他的方式嗎?
A:還有就是兩種餓漢模式。
餓漢實現單例模式:
餓漢變種實現單例模式:
Q:那你上面提到的幾種都是線程安全的嗎?
A:是線程安全的
Q:那是如何做到線程安全的呢?
A:應該是因為我使用了static,然後類載入的時候就線程安全了吧?
Q:其實你說的並不完全對,因為以上幾種雖然沒有直接使用synchronized,但是也是間接用到了。
(這裡面根據回答情況會朝兩個不同的方向展開:1、類載入機制、模塊化等;2、繼續深入問單例模式)
類載入過程的線程安全性保證
以上的靜態內部類、餓漢等模式均是通過定義靜態的成員變數,以保證單例對象可以在類初始化的過程中被實例化。
這其實是利用了ClassLoader的線程安全機制。ClassLoader的loadClass方法在載入類的時候使用了synchronized關鍵字。
所以, 除非被重寫,這個方法預設在整個裝載過程中都是線程安全的。所以在類載入過程中對象的創建也是線程安全的。
Q:那還回到剛開始的問題,你知道怎麼不使用synchronized和lock實現一個線程安全的單例嗎?
(並不是故意窮追不捨,而是希望能可以引發麵試者的更多思考)
A:額、、、那枚舉吧,枚舉也可以實現單例。
枚舉實現單例模式:
Q:那你知道枚舉單例的原理嗎?如何保證線程安全的呢?
枚舉單例的線程安全問題
枚舉其實底層是依賴Enum類實現的,這個類的成員變數都是static類型的,並且在靜態代碼塊中實例化的,和餓漢有點像, 所以他天然是線程安全的。
Q:所以,枚舉其實也是藉助了synchronized的,那你知道哪種方式可以完全不使用synchronized的嗎?
A:en....我想想
Q:(過了一會他好像沒有思路)你知道CAS嗎?使用CAS可以實現單例嗎?
(面試中,如果面試者對於鎖比較瞭解的話,那我大多數情況下都會繼續朝兩個方向深入問:1、鎖的實現原理;2、非鎖,如CAS、ThreadLocal等)
A:哦,我知道,CAS是一項樂觀鎖技術,當多個線程嘗試使用CAS同時更新一個變數時,只有其中一個線程能更新成功。
藉助CAS(AtomicReference)實現單例模式:
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton() {}
public static Singleton getInstance() {
for (;;) {
Singleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new Singleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
Q:使用CAS實現的單例有沒有什麼優缺點呀?
A:用CAS的好處在於不需要使用傳統的鎖機制來保證線程安全,CAS是一種基於忙等待的演算法,依賴底層硬體的實現,相對於鎖它沒有線程切換和阻塞的額外消耗,可以支持較大的並行度。
Q:你說的好像是優點?那缺點呢?
CAS實現的單例的缺點
CAS的一個重要缺點在於如果忙等待一直執行不成功(一直在死迴圈中),會對CPU造成較大的執行開銷。
另外,代碼中,如果N個線程同時執行到 singleton = new Singleton();的時候,會有大量對象被創建,可能導致記憶體溢出。
Q:好的,除了使用CAS以外,你還知道有什麼辦法可以不使用synchronized實現單例嗎?
A:這回真的不太知道了。
Q:(那我再提醒他一下吧)可以考慮下ThreadLocal,看看能不能實現?
(面試者沒有思路的時候,我幾乎都會先做一下提醒,實在沒有思路再換下一個問題)
A:ThreadLocal?這也可以嗎?
Q:你先說下你理解的ThreadLocal是什麼吧
(通過他的回答,貌似對這個思路有些疑惑,不著急。先問一個簡單的問題,讓面試者放鬆一下,找找自信,然後再繼續問)
ThreadLoacal
ThreadLocal會為每一個線程提供一個獨立的變數副本,從而隔離了多個線程對數據的訪問衝突。對於多線程資源共用的問題,同步機制(synchronized)採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。
同步機制僅提供一份變數,讓不同的線程排隊訪問,而ThreadLocal為每一個線程都提供了一份變數,因此可以同時訪問而互不影響。
Q:那理論上是不是可以使用ThreadLocal來實現單例呢?
A:應該也是可行的。
使用ThreadLocal實現單例模式:
Q:嗯嗯,好的,那有關單例模式的實現的問題我就問的差不多了。
(ThreadLocal這種寫法主要是考察面試者對於ThreadLocal的理解,以及是否可以把知識活學活用,但是實際上,這種所謂的"單例",其實失去了單例的意義...)
(但是說實話,能回答到這一題的人很少,大多數面試者基本上在前面幾道題就已經沒有思路了,大多數情況下根本不會問到這個問題就要改方向了)
A:(心中竊喜)嗯嗯,學習到很多,感謝
Q:那...你知道如何破壞單例嗎?
(單例問題,必問的一個。通過這個引申到序列化和反射的相關知識)
A:(額....)
最後這裡小編整理了一套面試資料,讓你面試不慌張