單例模式是面試中常常會被問到的一個問題,網上有大量的文章介紹單例模式的實現,本文也是參考那些優秀的文章來作一個總結,經過本身在學習過程當中的理解進行記錄,並補充完善一些內容,一方面鞏固本身所學的內容,另外一方面但願能對其餘同窗提供一些幫助。html
本文主要從如下幾個方面介紹單例模式:java
23 種設計模式能夠分爲三大類:建立型模式、行爲型模式、結構型模式。單例模式屬於建立型模式的一種,單例模式是最簡單的設計模式之一:單例模式只涉及一個類,確保在系統中一個類只有一個實例,並提供一個全局訪問入口。許多時候整個系統只須要擁有一個全局對象,這樣有利於咱們協調系統總體的行爲。面試
一、 日誌類spring
日誌類一般做爲單例實現,並在全部應用程序組件中提供全局日誌訪問點,而無需在每次執行日誌操做時建立對象。數據庫
二、 配置類設計模式
將配置類設計爲單例實現,好比在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,而後服務進程中的其餘對象再經過這個單例對象獲取這些配置信息,這種方式簡化了在複雜環境下的配置管理。安全
三、工廠類服務器
假設咱們設計了一個帶有工廠的應用程序,以在多線程環境中生成帶有 ID 的新對象(Acount、Customer、Site、Address 對象)。若是工廠在 2 個不一樣的線程中被實例化兩次,那麼 2 個不一樣的對象可能有 2 個重疊的 id。若是咱們將工廠實現爲單例,咱們就能夠避免這個問題,結合抽象工廠或工廠方法和單例設計模式是一種常見的作法。多線程
四、以共享模式訪問資源的類性能
好比網站的計數器,通常也是採用單例模式實現,若是你存在多個計數器,每個用戶的訪問都刷新計數器的值,這樣的話你的實計數的值是難以同步的。可是若是採用單例模式實現就不會存在這樣的問題,並且還能夠避免線程安全問題。
五、在Spring中建立的Bean實例默認都是單例模式存在的。
適用場景:
優勢:
缺點:
實現單例模式的步驟以下:
餓漢式單例模式,顧名思義,類一加載就建立對象,這種方式比較經常使用,但容易產生垃圾對象,浪費內存空間。
優勢:線程安全,沒有加鎖,執行效率較高
缺點:不是懶加載,類加載時就初始化,浪費內存空間
懶加載 (lazy loading):使用的時候再建立對象
餓漢式單例是如何保證線程安全的呢?它是基於類加載機制避免了多線程的同步問題,可是若是類被不一樣的類加載器加載就會建立不一樣的實例。
代碼實現,以及使用反射破壞單例:
/** * 餓漢式單例測試 * * @className: Singleton * @date: 2021/6/7 14:32 */ public class Singleton { // 一、私有化構造方法 private Singleton(){} // 二、定義一個靜態變量指向本身類型 private final static Singleton instance = new Singleton(); // 三、對外提供一個公共的方法獲取實例 public static Singleton getInstance() { return instance; } }
使用反射破壞單例,代碼以下:
public class Test { public static void main(String[] args) throws Exception{ // 使用反射破壞單例 // 獲取空參構造方法 Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null); // 設置強制訪問 declaredConstructor.setAccessible(true); // 建立實例 Singleton singleton = declaredConstructor.newInstance(); System.out.println("反射建立的實例" + singleton); System.out.println("正常建立的實例" + Singleton.getInstance()); System.out.println("正常建立的實例" + Singleton.getInstance()); } }
輸出結果以下:
反射建立的實例com.example.spring.demo.single.Singleton@6267c3bb 正常建立的實例com.example.spring.demo.single.Singleton@533ddba 正常建立的實例com.example.spring.demo.single.Singleton@533ddba
這種方式在單線程下使用沒有問題,對於多線程是沒法保證單例的,這裏列出來是爲了和後面使用鎖保證線程安全的單例作對比。
優勢:懶加載
缺點:線程不安全
代碼實現以下:
/** * 懶漢式單例,線程不安全 * * @className: Singleton * @date: 2021/6/7 14:32 */ public class Singleton { // 一、私有化構造方法 private Singleton(){ } // 二、定義一個靜態變量指向本身類型 private static Singleton instance; // 三、對外提供一個公共的方法獲取實例 public static Singleton getInstance() { // 判斷爲 null 的時候再建立對象 if (instance == null) { instance = new Singleton(); } return instance; } }
使用多線程破壞單例,測試代碼以下:
public class Test { public static void main(String[] args) { for (int i = 0; i < 3; i++) { new Thread(() -> { System.out.println("多線程建立的單例:" + Singleton.getInstance()); }).start(); } } }
輸出結果以下:
多線程建立的單例:com.example.spring.demo.single.Singleton@18396bd5 多線程建立的單例:com.example.spring.demo.single.Singleton@7f23db98 多線程建立的單例:com.example.spring.demo.single.Singleton@5000d44
懶漢式單例如何保證線程安全呢?經過 synchronized
關鍵字加鎖保證線程安全,synchronized
能夠添加在方法上面,也能夠添加在代碼塊上面,這裏演示添加在方法上面,存在的問題是每一次調用 getInstance
獲取實例時都須要加鎖和釋放鎖,這樣是很是影響性能的。
優勢:懶加載,線程安全
缺點:效率較低
代碼實現以下:
/** * 懶漢式單例,方法上面添加 synchronized 保證線程安全 * * @className: Singleton * @date: 2021/6/7 14:32 */ public class Singleton { // 一、私有化構造方法 private Singleton(){ } // 二、定義一個靜態變量指向本身類型 private static Singleton instance; // 三、對外提供一個公共的方法獲取實例 public synchronized static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
實現代碼以下:
/** * 雙重檢查鎖(DCL, 即 double-checked locking) * * @className: Singleton * @date: 2021/6/7 14:32 */ public class Singleton { // 一、私有化構造方法 private Singleton() { } // 二、定義一個靜態變量指向本身類型 private volatile static Singleton instance; // 三、對外提供一個公共的方法獲取實例 public synchronized static Singleton getInstance() { // 第一重檢查是否爲 null if (instance == null) { // 使用 synchronized 加鎖 synchronized (Singleton.class) { // 第二重檢查是否爲 null if (instance == null) { // new 關鍵字建立對象不是原子操做 instance = new Singleton(); } } } return instance; } }
優勢:懶加載,線程安全,效率較高
缺點:實現較複雜
這裏的雙重檢查是指兩次非空判斷,鎖指的是 synchronized 加鎖,爲何要進行雙重判斷,其實很簡單,第一重判斷,若是實例已經存在,那麼就再也不須要進行同步操做,而是直接返回這個實例,若是沒有建立,纔會進入同步塊,同步塊的目的與以前相同,目的是爲了防止有多個線程同時調用時,致使生成多個實例,有了同步塊,每次只能有一個線程調用訪問同步塊內容,當第一個搶到鎖的調用獲取了實例以後,這個實例就會被建立,以後的全部調用都不會進入同步塊,直接在第一重判斷就返回了單例。
關於內部的第二重空判斷的做用,當多個線程一塊兒到達鎖位置時,進行鎖競爭,其中一個線程獲取鎖,若是是第一次進入則爲 null,會進行單例對象的建立,完成後釋放鎖,其餘線程獲取鎖後就會被空判斷攔截,直接返回已建立的單例對象。
其中最關鍵的一個點就是 volatile
關鍵字的使用,關於 volatile
的詳細介紹能夠直接搜索 volatile 關鍵字便可,有不少寫的很是好的文章,這裏不作詳細介紹,簡單說明一下,雙重檢查鎖中使用 volatile
的兩個重要特性:可見性、禁止指令重排序
這裏爲何要使用 volatile
?
這是由於 new
關鍵字建立對象不是原子操做,建立一個對象會經歷下面的步驟:
對應字節碼指令以下:
爲了提升性能,編譯器和處理器經常會對既定的代碼執行順序進行指令重排序,從源碼到最終執行指令會經歷以下流程:
graph LR A[源碼] -->B([編譯器優化重排序])-->C([指令級並行重排序])-->D([內存系統重排序])-->E[最終執行指令序列]
因此通過指令重排序以後,建立對象的執行順序可能爲 1 2 3
或者 1 3 2
,所以當某個線程在亂序運行 1 3 2
指令的時候,引用變量指向堆內存空間,這個對象不爲 null,可是沒有初始化,其餘線程有可能這個時候進入了 getInstance
的第一個 if(instance == null)
判斷不爲 nulll ,致使錯誤使用了沒有初始化的非 null 實例,這樣的話就會出現異常,這個就是著名的 DCL 失效問題。
當咱們在引用變量上面添加 volatile
關鍵字之後,會經過在建立對象指令的先後添加內存屏障來禁止指令重排序,就能夠避免這個問題,並且對 volatile
修飾的變量的修改對其餘任何線程都是可見的。
代碼實現以下:
/** * 靜態內部類實現單例 * * @className: Singleton * @date: 2021/6/7 14:32 */ public class Singleton { // 一、私有化構造方法 private Singleton() { } // 二、對外提供獲取實例的公共方法 public static Singleton getInstance() { return InnerClass.INSTANCE; } // 定義靜態內部類 private static class InnerClass{ private final static Singleton INSTANCE = new Singleton(); } }
優勢:懶加載,線程安全,效率較高,實現簡單
靜態內部類單例是如何實現懶加載的呢?首先,咱們先了解下類的加載時機。
虛擬機規範要求有且只有5種狀況必須當即對類進行初始化(加載、驗證、準備須要在此以前開始):
new
、getstatic
、putstatic
、invokestatic
這4條字節碼指令時。生成這4條指令最多見的 Java 代碼場景是:使用 new
關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(final修飾除外,被final修飾的靜態字段是常量,已在編譯期把結果放入常量池)的時候,以及調用一個類的靜態方法的時候。java.lang.reflect
包方法對類進行反射調用的時候。java.lang.invoke.MethodHandle
實例最後的解析結果是REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,則須要先觸發這個方法句柄所對應的類的初始化。這5種狀況被稱爲是類的主動引用,注意,這裏《虛擬機規範》中使用的限定詞是 "有且僅有",那麼,除此以外的全部引用類都不會對類進行初始化,稱爲被動引用。靜態內部類就屬於被動引用的狀況。
當getInstance()方法被調用時,InnerClass 纔在 Singleton 的運行時常量池裏,把符號引用替換爲直接引用,這時靜態對象 INSTANCE 也真正被建立,而後再被 getInstance()方法返回出去,這點同餓漢模式。
那麼 INSTANCE
在建立過程當中又是如何保證線程安全的呢?在《深刻理解JAVA虛擬機》中,有這麼一句話:
虛擬機會保證一個類的 <clinit>()
方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的 <clinit>()
方法,其餘線程都須要阻塞等待,直到活動線程執行 <clinit>()
方法完畢。若是在一個類的 <clinit>()
方法中有耗時很長的操做,就可能形成多個進程阻塞(須要注意的是,其餘線程雖然會被阻塞,但若是執行<clinit>()
方法後,其餘線程喚醒以後不會再次進入<clinit>()
方法。同一個加載器下,一個類型只會初始化一次。),在實際應用中,這種阻塞每每是很隱蔽的。
從上面的分析能夠看出INSTANCE在建立過程當中是線程安全的,因此說靜態內部類形式的單例可保證線程安全,也能保證單例的惟一性,同時也延遲了單例的實例化。
代碼實現以下:
/** * 枚舉實現單例 * * @className: Singleton * @date: 2021/6/7 14:32 */ public enum Singleton { INSTANCE; public void doSomething(String str) { System.out.println(str); } }
優勢:簡單,高效,線程安全,能夠避免經過反射破壞枚舉單例
枚舉在java中與普通類同樣,都能擁有字段與方法,並且枚舉實例建立是線程安全的,在任何狀況下,它都是一個單例,能夠直接經過以下方式調用獲取實例:
Singleton singleton = Singleton.INSTANCE;
使用下面的命令反編譯枚舉類
javap Singleton.class
獲得以下內容
Compiled from "Singleton.java" public final class com.spring.demo.singleton.Singleton extends java.lang.Enum<com.spring.demo.singleton.Singleton> { public static final com.spring.demo.singleton.Singleton INSTANCE; public static com.spring.demo.singleton.Singleton[] values(); public static com.spring.demo.singleton.Singleton valueOf(java.lang.String); public void doSomething(java.lang.String); static {}; }
從枚舉的反編譯結果能夠看到,INSTANCE 被 static final
修飾,因此能夠經過類名直接調用,而且建立對象的實例是在靜態代碼塊中建立的,由於 static 類型的屬性會在類被加載以後被初始化,當一個Java類第一次被真正使用到的時候靜態資源被初始化、Java類的加載和初始化過程都是線程安全的,因此建立一個enum類型是線程安全的。
經過反射破壞枚舉,實現代碼以下:
public class Test { public static void main(String[] args) throws Exception { Singleton singleton = Singleton.INSTANCE; singleton.doSomething("hello enum"); // 嘗試使用反射破壞單例 // 枚舉類沒有空參構造方法,反編譯後能夠看到枚舉有一個兩個參數的構造方法 Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class, int.class); // 設置強制訪問 declaredConstructor.setAccessible(true); // 建立實例,這裏會報錯,由於沒法經過反射建立枚舉的實例 Singleton enumSingleton = declaredConstructor.newInstance(); System.out.println(enumSingleton); } }
運行結果報以下錯誤:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492) at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) at com.spring.demo.singleton.Test.main(Test.java:24)
查看反射建立實例的 newInstance()
方法,有以下判斷:
因此沒法經過反射建立枚舉的實例。
在java中,若是一個Singleton類實現了java.io.Serializable接口,當這個singleton被屢次序列化而後反序列化時,就會建立多個Singleton類的實例。爲了不這種狀況,應該實現 readResolve 方法。請參閱 javadocs 中的 Serializable () 和 readResolve Method () 。
public class Singleton implements Serializable { // 一、私有化構造方法 private Singleton() { } // 二、對外提供獲取實例的公共方法 public static Singleton getInstance() { return InnerClass.instance; } // 定義靜態內部類 private static class InnerClass{ private final static Singleton instance = new Singleton(); } // 對象被反序列化以後,這個方法當即被調用,咱們重寫這個方法返回單例對象. protected Object readResolve() { return getInstance(); } }
使用單例設計模式須要注意的點:
本文簡單介紹了單例設計模式的幾種實現方式,除了枚舉單例,其餘的全部實現均可以經過反射破壞單例模式,在《effective java》中推薦枚舉實現單例模式,在實際場景中使用哪種單例實現,須要根據本身的狀況選擇,適合當前場景的纔是比較好的方式。
參考文章