作者: "DeppWang" 、 "原文地址" 人生在世,誰不面試。單例模式:一個搞懂不加分,不搞懂減分的知識點 又一篇一抓一大把的博文,可是你真的的搞懂了嗎?點開看看。。事後,你也來一篇。 單例模式是面試中非常喜歡問的了,我們往往自認為已經完全理解了,沒什麼問題了。但要把它手寫出來的時候,可能出現 ...
人生在世,誰不面試。單例模式:一個搞懂不加分,不搞懂減分的知識點
又一篇一抓一大把的博文,可是你真的的搞懂了嗎?點開看看。。事後,你也來一篇。
單例模式是面試中非常喜歡問的了,我們往往自認為已經完全理解了,沒什麼問題了。但要把它手寫出來的時候,可能出現各種小錯誤,下麵是我總結的快速準確的寫出單例模式的方法。
單例模式有各種寫法,什麼「雙重檢鎖法」、什麼「餓漢式」、什麼「飽漢式」,總是記不住、分不清。這就對了,人的記憶力是有限的,我們應該記的是最基本的單例模式怎麼寫。
單例模式:一個類有且只能有一個對象(實例)。單例模式的 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 就是通過單例模式創建出來的。
單例模式沒寫出來,那也沒啥事,因為那下一個問題你也不一定能答出來