單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於建立型模式,它提供了一種建立對象的最佳方式。java
這種模式涉及到一個單一的類,該類負責建立本身的對象,同時確保只有單個對象被建立。這個類提供了一種訪問其惟一的對象的方式,能夠直接訪問,不須要實例化該類的對象。面試
【注意】
💥 單例類只能有一個實例。
💥 單例類必須本身建立本身的惟一實例。
💥 單例類必須給全部其餘對象提供這一實例。數據庫
咱們先來看看關於「單例模式」的如下幾點:設計模式
單例意圖:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。緩存
主要解決:一個全局使用的類頻繁地建立與銷燬。安全
什麼時候使用:當您想控制實例數目,節省系統資源的時候。多線程
如何解決:判斷系統是否已經有這個單例,若是有則返回,若是沒有則建立。函數
關鍵代碼:構造函數是私有的。性能
單例優勢: 一、在內存裏只有一個實例,減小了內存的開銷,尤爲是頻繁的建立和銷燬實例;二、避免對資源的多重佔用(好比寫文件操做)。優化
單例缺點:沒有接口,不能繼承,與單一職責原則衝突,一個類應該只關心內部邏輯,而不關心外面怎麼樣來實例化。
使用場景: 一、要求生產惟一序列號;二、WEB 中的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來;三、建立的一個對象須要消耗的資源過多,好比 I/O 與數據庫的鏈接等。
注意事項:getInstance()方法中須要使用同步鎖 synchronized (Singleton.class) 防止多線程同時進入形成 instance 被屢次實例化。
接下來咱們看個簡單的單例設計的 Demo,咱們先建立一個 Singleton 類:SingleObject.java
public class SingleObject { //建立 SingleObject 的一個對象 private static SingleObject instance = new SingleObject(); //讓構造函數爲 private,這樣該類就不會被實例化 private SingleObject(){} //獲取惟一可用的對象 public static SingleObject getInstance(){ return instance; } public void showMessage(){ System.out.println("Hello World!"); } }
而後從 singleton 類獲取惟一的對象:SingletonPatternDemo.java
public class SingletonPatternDemo { public static void main(String[] args) { //不合法的構造函數 //編譯時錯誤:構造函數 SingleObject() 是不可見的 //SingleObject object = new SingleObject(); //獲取惟一可用的對象 SingleObject object = SingleObject.getInstance(); //顯示消息 object.showMessage(); } }
咱們看下執行結果:
Hello World!
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
這種方式比較常見,典型的「餓漢式」寫法。
【是否多線程安全】:是
【實現難度】:易
【優勢】:沒有加鎖,執行效率會提升。
【缺點】:類加載時就初始化,浪費內存。
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
這種方式是大多數面試者的寫法,也是教科書上的標配,但這段代碼卻存在一個致命的問題:當多個線程並行調用 getInstance() 的時候,就會建立多個實例。
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
既然要線程安全,那就如上所述「加鎖」處理!
【是否多線程安全】:是
【實現難度】:易
【優勢】:第一次調用才初始化,避免內存浪費。
【缺點】:必須加鎖 synchronized 才能保證單例,但加鎖(加鎖操做也是耗時的)會影響效率。
public class Singleton { private static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
爲何須要進行 2 次判斷是否爲空呢?
第一次判斷是爲了不沒必要要的同步,第二次判斷是確保在此以前沒有其餘進程進入到 synchronized 塊建立了新實例。
這段代碼看起來很完美,很惋惜,它仍是有隱患。主要在於 instance = new Singleton() 這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情:
✨ 給 instance 分配內存
✨ 調用 Singleton 的構造函數來初始化成員變量
✨ 將 instance 對象指向分配的內存空間(執行完這步 instance 就爲非 null 了)
可是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
有些人認爲使用 volatile 的緣由是可見性,也就是能夠保證線程在本地不會存有 instance 的副本,每次都是去主內存中讀取。但實際上是不對的。使用 volatile 的主要緣由是其另外一個特性:禁止指令重排序優化。也就是說,在 volatile 變量的賦值操做後面會有一個內存屏障(生成的彙編代碼上),讀操做不會被重排序到內存屏障以前。好比上面的例子,取操做必須在執行完 1-2-3 以後或者 1-3-2 以後,不存在執行到 1-3 而後取到值的狀況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變量的寫操做都先行發生於後面對這個變量的讀操做(這裏的「後面」是時間上的前後順序)。
可是特別注意在 Java 5 之前的版本使用了 volatile 的雙檢鎖仍是有問題的。其緣由是 Java 5 之前的 JMM (Java 內存模型)是存在缺陷的,即時將變量聲明成 volatile 也不能徹底避免重排序,主要是 volatile 變量先後的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復,因此在這以後才能夠放心使用 volatile。
那麼,有沒有一種既有懶加載,又保證了線程安全,還簡單的方法呢?
固然有,靜態內部類,就是一種咱們想要的方法。咱們徹底能夠把 Singleton 實例放在一個靜態內部類中,這樣就避免了靜態實例在 Singleton 類加載的時候就建立對象,而且因爲靜態內部類只會被加載一次,因此這種寫法也是線程安全的。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
這是比較推薦的解法,這種寫法用 JVM 自己的機制保證了線程安全的問題,同時讀取實例的時候也不會進行同步,沒什麼性能缺陷,還不依賴 JDK 版本。
public enum Singleton { INSTANCE; }
這是從 Java 1.5 發行版本後就能夠實用的單例方法,咱們能夠經過 Singleton.INSTANCE 來訪問實例,這比調用 getInstance() 方法簡單多了。
建立枚舉默認就是線程安全的,因此不須要擔憂 double checked locking,並且還能防止反序列化致使從新建立新的對象。