此前面試遇到了單例問題,本覺得已經背的倒背如流,沒想到被問單例如何避免被反射和序列化破壞,雖而後來仍是等到了通知,但仍是複習一下單例的實現方式,並學習防止反射和序列化破壞的手段。java
其餘相關資料中,最多的能數出八種單例實現方式,而實際上其中有些實現並不具有實際意義,在文中出現也僅是爲了指出存在的問題便於引出下文。本文僅介紹有實際意義的單例實現模式。爲了縮減篇幅,先給出一個後續出現代碼的模板的類圖:git
單例類 Singleton 模板:後文中介紹具體實現方式僅給出 Singleton#instance
引用和 Singleton#getInstance
方法的內容,其餘內容無變化。github
public class Singleton { private static final Logger log = LogManager.getLogger(Singleton.class); //單例引用,不一樣實現方式有所不一樣 private static Singleton instance; /** * 獲取單例的函數,不一樣實現方式有所不一樣 * * @return 單例 */ public static Singleton getInstance() { //some code } /** * 靜態方法,用於觸發虛擬機類加載,僅有一行日誌用於觀察類加載時間 */ public static void load() { log.debug("{} loaded", Singleton.class); } /** * 單例類的功能函數,僅有一行日誌 */ public void function() { log.debug("Singleton's instance using"); } /** * 私有的構造函數,僅有一行日誌用於觀察構造時間 */ private Singleton() { log.debug("Singleton's instance instantiated"); } }
調用單例類的 Main 類:面試
public class Main { public static void main(String[] args) throws Exception{ //先觸發類加載 Singleton.load(); //等待必定時間 TimeUnit.SECONDS.sleep(3); //執行單例的功能函數 Singleton.getInstance().function(); } }
餓漢式具備線程安全和非 Lazy 初始化的特色,實現難度最簡單。因爲 JVM 的類加載是單線程的,且已加載過的類不會重複加載,因此餓漢式天生具備線程安全的特色。設計模式
因爲是類加載即初始化,單例引用可添加 final
修飾。安全
public static final Singleton instance = new Singleton(); //下面寫法效果相同 /* public static final Singleton instance; static { instance = new Singleton(); } */
獲取單例函數:函數
public static Singleton getInstance() { return instance; }
執行結果:學習
13:19:32.565 - Singleton's instance instantiated 13:19:32.569 - class cncsl.github.io.Singleton loaded 13:19:35.572 - Singleton's instance using
從日誌能夠看出,單例類剛加載時就調用構造函數完成了單實例的初始化。線程
雙鎖檢查是常常出現於面試題中的實現方式,具備線程安全和 Lazy 初始化的特色。須要自行實現線程安全的單例初始化,且要避免指令重排序致使的安全問題,實現難度較高。debug
爲了不指令重排序致使的線程安全問題,須要給單例引用添加 volatile
修飾:
private volatile static Singleton instance;
雙鎖檢查式最難的部分就是在獲取單例的函數中進行兩次非 null 判斷和加鎖後再初始化的過程:
public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }
執行結果:
13:30:07.524 - class cncsl.github.io.Singleton loaded 13:30:10.541 - Singleton's instance instantiated 13:30:10.541 - Singleton's instance using
從日誌能夠看出,類加載以後並無當即初始化,實際須要調用到單例的功能函數前才進行了初始化。
靜態內部類式也用到了 JVM 類加載器的特性,既保證線程安全的狀況下實現了 Lazy 加載。
添加一個靜態內部類持有單例引用:
private static class InstanceHolder { private static final Singleton INSTANCE = new Singleton(); }
獲取單例的函數調用時纔會加載靜態內部類,進而觸發單實例的初始化:
public static Singleton getInstance() { return InstanceHolder.INSTANCE; }
執行效果與雙鎖檢查式相同,不在贅述。
枚舉式的實現是將單例類寫成一個枚舉,枚舉值僅包含一個單例引用,再加上與業務邏輯相關的功能函數便可。因爲枚舉的特色,這種實現方式具備線程安全、非 Lazy 加載和防止反射、序列化破壞單例等特色。
因爲改動較大附上所有代碼:
public enum Singleton { /** * 單例枚舉值 */ INSTANCE; /** * 獲取單例的函數 */ public static Singleton getInstance() { return INSTANCE; } /** * 靜態方法,用於觸發虛擬機類加載,僅有一行日誌用於觀察類加載時間 */ public static void load() { System.out.printf("%s - %s loaded%n", LocalTime.now().toString(), Singleton.class); } /** * 單例類的功能函數,僅有一行日誌 */ public void function() { System.out.printf("%s - Singleton's instance using%n", LocalTime.now().toString()); } /** * 私有的構造函數,僅有一行日誌用於觀察構造時間 */ Singleton() { System.out.printf("%s - Singleton's instance instantiated%n", LocalTime.now().toString()); } }
執行結果:
13:57:27.142 - Singleton's instance instantiated 13:57:27.154 - class cncsl.github.io.Singleton loaded 13:57:30.156 - Singleton's instance using
能夠看出,枚舉類加載以後當即初始化了單例對象,而三秒後執行了單例類的功能函數。
在 Java 中,經過序列化也能建立新的對象實例,而反射能突破構造函數 private
的限制,下面介紹一下如何避免這些狀況的發生。枚舉式單例天生避免了這些問題,下方內容都是針對其餘三種實現方式而言的。
另外,請明白一個前提,設計模式是一種設計的方式,既不是某種語言的語法約束,除了枚舉方式之外、其餘實現方式在有人惡意破壞的狀況都沒法徹底確保單例。在這種狀況下,須要考慮的不是如何改進現有的設計,而是找出企圖經過這些手段破壞單例的人。因此下面的知識通常用於面試:當遇到如何確保單例的問題時,首先說枚舉式設計方式、而後纔是下面的內容。
下方是經過反射方式破壞單例的過程:
public static void main(String[] args) { try { //經過getInstance()獲取 Singleton one = Singleton.getInstance(); log.debug(one.hashCode()); //反射調用構造函數 Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton two = constructor.newInstance(); log.debug(two.hashCode()); log.debug(one == two); } catch (Exception e) { log.error("Exception: ", e); } }
執行結果:
15:10:48.237 - Singleton's instance instantiated 15:10:48.240 - 1159114532 15:10:48.240 - Singleton's instance instantiated 15:10:48.240 - 1832580921 15:10:48.240 - false
能夠看出目前程序中以存在兩個 Singleton 類的實例,單例已經被破壞。
解決方案爲在單例類的構造函數中進行檢查,若是單例引用不爲 null 就拋出異常:
private Singleton() { if (instance != null) { throw new UnsupportedOperationException("不容許重複建立實例"); } log.debug("Singleton's instance instantiated"); }
再次執行結果:
15:17:36.834 - Singleton's instance instantiated 15:17:36.836 - 1159114532 15:17:36.836 - Exception: java.lang.reflect.InvocationTargetException: null at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[?:1.8.0_261] at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[?:1.8.0_261] at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[?:1.8.0_261] at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[?:1.8.0_261] at cncsl.github.io.Main.main(Main.java:18) [classes/:?] Caused by: java.lang.UnsupportedOperationException: 不容許重複建立實例 at cncsl.github.io.Singleton.<init>(Singleton.java:32) ~[classes/:?] ... 5 more
固然,攻擊者能夠在外部先記錄一份 instance
引用,經過反射修改 instance
引用後再建立對象,這樣程序中會存在兩個 Singleton
實例。
下方是經過序列化手段破壞單例的過程:
public static void main(String[] args) { try (ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("Singleton.temp")); ObjectInputStream input = new ObjectInputStream(new FileInputStream("Singleton.temp"))) { Singleton one = Singleton.getInstance(); log.debug(one.hashCode()); output.writeObject(one); Singleton two = (Singleton) input.readObject(); log.debug(two.hashCode()); log.debug(one == two); } catch (Exception e) { log.error("Exception: ", e); } }
執行結果以下:
21:15:36.605 - Singleton's instance instantiated 21:15:36.610 - 22756955 21:15:36.619 - 1582785598 21:15:36.619 - false
能夠看出,序列化讀取到的對象已是一個新的對象,單例已被破壞。
解決方案是爲單例類添加以下函數:
private Object readResolve() { return instance; }
再次執行後能夠發現已經反序列化時獲得的仍然是原單例對象:
21:28:34.954 - Singleton's instance instantiated 21:28:34.956 - 22756955 21:28:34.964 - 22756955 21:28:34.964 - true
當前序列化有個前提是實現 Serializable
接口,私覺得這種狀況是一個錯誤的設計:單例類通常和業務邏輯相關、而序列化通常和封裝數據用的實體對象有關,兩者不該該出如今同一個類裏。