人生在世,誰不面試。單例模式:一個搞懂不加分,不搞懂減分的知識點
又一篇一抓一大把的博文,但是你真的的搞懂了嗎?點開看看。。過後,你也來一篇。java
單例模式是面試中很是喜歡問的了,咱們每每自認爲已經徹底理解了,沒什麼問題了。但要把它手寫出來的時候,可能出現各類小錯誤,下面是我總結的快速準確的寫出單例模式的方法。git
單例模式有各類寫法,什麼「雙重檢鎖法」、什麼「餓漢式」、什麼「飽漢式」,老是記不住、分不清。這就對了,人的記憶力是有限的,咱們應該記的是最基本的單例模式怎麼寫。github
單例模式:一個類有且只能有一個對象(實例)。單例模式的 3 個要點:面試
private Singleton(){}
public static Singleton getInstance()
private static Singleton instance
<!--more-->安全
類加載的時候就新建實例多線程
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 不能指向另外一個實例,這裏用不上。固然,加上也沒問題。函數
<!--// final 修飾的基本數據類型,在編譯期時,初始化數據放在常量池-->this
這個寫法有一個不足之處,就是若是須要經過參數設置實例,則沒法作到。舉個栗子:
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 步執行。
若是 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 就是經過單例模式建立出來的。
單例模式沒寫出來,那也沒啥事,由於那下一個問題你也不必定能答出來 :)。
本篇文章由一文多發平臺ArtiPub自動發佈