人生在世,誰不面試。單例模式:一個搞懂不加分,不搞懂減分的知識點
又一篇一抓一大把的博文,可是你真的的搞懂了嗎?點開看看。。事后,你也來一篇。
單例模式是面試中非常喜歡問的了,我們往往自認為已經完全理解了,沒什么問題了。但要把它手寫出來的時候,可能出現各種小錯誤,下面是我總結的快速準確的寫出單例模式的方法。
單例模式有各種寫法,什么「雙重檢鎖法」、什么「餓漢式」、什么「飽漢式」,總是記不住、分不清。這就對了,人的記憶力是有限的,我們應該記的是最基本的單例模式怎么寫。
單例模式:一個類有且只能有一個對象(實例)。單例模式的 3 個要點:
- 外部不能通過 new 關鍵字(構造函數)的方式新建實例,所以構造函數為私有:
private Singleton(){}
- 只能通過類方法獲取實例,所以獲取實例的方法為公有、且為靜態:
public static Singleton getInstance()
- 實例只能有一個,那只能作為類變量的「數據」,類變量為靜態 (另一種記憶:靜態方法只能使用靜態變量):
private static Singleton instance
一、最基礎、最簡單的寫法
類加載的時候就新建實例
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
public void show(){
System.out.println("Singleon using static initialization in Java");
}
}
// Here is how to access this Singleton class
Singleton.getInstance().show();
當執行 Singleton.getInstance() 時,類加載器加載 Singleton.class 進虛擬機,虛擬機在方法區(元數據區)為類變量分配一塊內存,并賦值為空。再執行 <client>()
方法,新建實例指向類變量 instance。這個過程在類加載階段執行,并由虛擬機保證線程安全。所以執行 getInstance() 前,實例就已經存在,所以 getInstance() 是線程安全的。
很多博文說 instance 還需要聲明為 final,其實不用。final 的作用在于不可變,使引用 instance 不能指向另一個實例,這里用不上。當然,加上也沒問題。
這個寫法有一個不足之處,就是如果需要通過參數設置實例,則無法做到。舉個栗子:
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
// 不能設置 name!
public static Singleton getInstance(String name) {
return instance;
}
public void show(){
System.out.println("Singleon using static initialization in Java");
}
}
// Here is how to access this Singleton class
Singleton.getInstance(String name).show();
二、可通過參數設置實例的寫法
考慮到這種情況,就在調用 getInstance() 方法時,再新建實例。
public class Singleton {
private static Singleton instance;
private String name;
private Singleton(String name) {
this.name = name;
}
public static synchronized Singleton getInstance(String name) {
if (instance == null) {
instance = new Singleton(name);
}
return instance;
}
public String show() {
return name;
}
}
Singleton.getInstance(String name).show();
這里加了 synchronized
關鍵字,能保證只會生成一個實例,但效率不高。因為實例創建成功后,再獲取實例時就不用加鎖了。
當不加 synchronized 時,會發生什么:
instance 是類的變量,類存放在方法區(元數據區),元數據區線程共享,所以類變量 instance 線程共享,類變量也是在主內存中。線程執行 getInstance() 時,在自己工作內存新建一個棧幀,將主內存的 instance 拷貝到工作內存。多個線程并發訪問時,都認為 instance == null
,就將新建多個實例,那單例模式就不是單例模式了。
三、改良版加鎖的寫法
實現只在創建的時候加鎖,獲取時不加鎖。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
為什么要判斷兩次:
多個線程將 instance 拷貝進工作內存,即多個線程讀取到 instance == null,雖然每次只有一個線程進入 synchronized 方法,當進入線程成功新建了實例,synchronized 保證了可見性(在 unlock 操作前將變量寫回了主內存),此時 instance 不等于 null 了,但其他線程已經執行到 synchronized 這里了,某個線程就又會進入 synchronized 方法,如果不判斷一次,又會再次新建一個實例。
為什么要用 volatile 修飾 instance:
synchronized 可以實現原子性、可見性、有序性。其中實現原子性:一次只有一個線程執行同步塊的代碼。但計算機為了提升運行效率,會指令重排序。
代碼 instance = new Singleton(); 會被拆為 3 步執行。
- A:分配一塊內存空間
- B:在內存空間位置新建一個實例
- C:將引用指向實例,即,引用存放實例的內存空間地址
如果 instance 都在 synchronized 里面,那么沒啥問題,問題出現在 instance 在 synchronized 外邊,因為此時外邊一群餓狼(線程),就在等待一個 instance 這塊肉不為 null。
模擬一下指令重排序的出錯場景:多線程環境下,正好一個線程,在同步塊中按 ACB 執行,執行到 AC 時(并將 instance 寫回了主內存),另一個線程執行第一個判斷時,認為 instance 不為空,返回 instance,但此時 instance 還沒被正確初始化,所以出錯。
當 instance 被 volatile 修飾時,只有 ACB 執行完了之后,其他線程才能讀取 instance
為什么 volatile 能禁止指令重排序:它在 ACB 后添加一個 lock 指令,lock 指令之前的操作執行完成后,后面的操作才能執行
你可能認為上面的解釋太復雜,不好理解。對,確實比較復雜,我也搞了很久才搞明白。你可以看看這個是不是更好理解,Java 虛擬機規范的其中一條先行發生原則:對 volatile 修飾的變量,讀操作,必須等寫操作完成。
四、其他非主流寫法
枚舉寫法:
public enum EasySingleton{
INSTANCE;
}
當面試官讓我寫一個單例模式,我總是覺得寫這個好像有點另類
靜態內部類寫法:
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
五、小結
單例模式主要為了節省內存開銷,Spring 容器的 Bean 就是通過單例模式創建出來的。
單例模式沒寫出來,那也沒啥事,因為那下一個問題你也不一定能答出來 :)。
六、延伸閱讀
- 如何正確寫出單例模式
- How to create thread safe Singleton in Java
- Why Enum Singleton are better in Java
- On design patterns: When should I use the singleton?
本文由博客一文多發平臺 OpenWrite 發布!