前言
如下是之前總結的 C++ 版的;軟件開發常用設計模式—單例模式總結(c++版),對比發現 Java 實現的單例模式和 C++ 的在線程安全上還是有些區別的。
概念不多說,沒意思,我自己總結就是:
有這樣一個類,該類在生命周期內有且只能有一個實例,該類必須自己創建自己的這個唯一實例,該類必須給所有其他對象提供這一實例(提供全局訪問點),這樣的類就叫單例類。
簡單的說就是滿足三個條件:
1、生命周期內有且只能有一個實例
2、自己提供這個獨一無二的實例
3、該實例必須是能全局訪問的
需要的考慮的細節
進一步,單例類,最好能實現懶加載,隨用隨生成,而不是初始化的時候就生成,提高啟動速度和優化內存。
還有應該考慮並發環境下的場景,多線程的單例模式實現有什么難點,回答這個問題,必須先知道Java的內存模型,參考:JVM學習(3)——總結Java內存模型
考慮黑客會做反序列化的攻擊
考慮黑客會做反射的攻擊,因為反射可以訪問私有方法
。。。
單線程環境下懶加載的單例
如果程序確認沒有多線程的使用場景,完全可以簡單一些寫。
public class NoThreadSafeLazySingleton { private static NoThreadSafeLazySingleton lazySingleton = null; private NoThreadSafeLazySingleton() { } public static NoThreadSafeLazySingleton getLazySingleton() { if (lazySingleton == null) { lazySingleton = new NoThreadSafeLazySingleton(); } return lazySingleton; } }
很簡單,但是只適用於單線程環境
線程安全的懶加載單例
原理也很簡單,沒什么可說的,如下示例代碼:
public class ThreadSafeLazySingleton { private static volatile ThreadSafeLazySingleton lazySingleton = null; private ThreadSafeLazySingleton() { } public static ThreadSafeLazySingleton getLazySingleton() { if (lazySingleton == null) { synchronized (ThreadSafeLazySingleton.class) { if (lazySingleton == null) { lazySingleton = new ThreadSafeLazySingleton(); } } } return lazySingleton; } }
主要是注意 volatile 關鍵字的使用,否則這種所謂雙重檢查的線程安全的單例是有 bug 的。參考:JVM學習(3)——總結Java內存模型
靜態內部類方案
在某些情況中,JVM 隱含了同步操作,這些情況下就不用自己再來進行同步控制了。這些情況包括:
-
由靜態初始化器(在靜態字段上或static{}塊中的初始化器)初始化數據時
-
訪問final字段時
-
在創建線程之前創建對象時
-
線程可以看見它將要處理的對象時
在靜態內部類里去創建本類(外部類)的對象,這樣只要不使用這個靜態內部類,那就不創建對象實例,從而同時實現延遲加載和線程安全。
public class Person { private String name; private Integer age; private Person() { } private Person(String name, Integer age) { this.name = name; this.age = age; } // 在靜態內部類里去創建本類(外部類)的對象 public static Person getInstance() { return Holder.instatnce; } // 靜態內部類相當於外部類 Person 的 static 域,它的對象與外部類對象間不存在依賴關系,因此可直接創建。 // 因為靜態內部類相當於其外部類 Person 的靜態成員,所以在第一次被使用的時候才被會裝載,且只裝載一次。 private static class Holder { // 內部類的對象實例 instatnce ,是綁定在外部 Person 對象實例中的 // 靜態內部類中可以定義靜態方法,在靜態方法中只能夠引用外部類中的靜態成員方法或者成員變量,比如 new Person // 使用靜態初始化器來實現線程安全的單例類,它由 JVM 來保證線程安全性。 private static final Person instatnce = new Person("John", 31); } }
靜態內部類相當於外部類 Person 的 static 域(靜態成員),它的對象與外部類對象間不存在依賴關系,因此可直接創建。
既然,靜態內部類相當於其外部類 Person 的靜態成員,所以在第一次被使用的時候才被會裝載,且只裝載一次,實現了懶加載和單例。
而且,使用靜態初始化器來實現單例類,是線程安全的,因為由 JVM 來保證線程安全性
客戶端調用
Person person = Person.getInstance();
該方案實現了,線程安全的單例 + 懶加載的單例,但是並不能防反序列化攻擊,需要額外的加以約束。
反序列化攻擊單例類
其實這個 case 沒必要說太多,知道就行,因為哪里就這么巧,一個能序列化的類(實現了Serializable/Externalizable接口的類),就恰恰是單例的呢?
看下面例子,把 Person 類改造為能序列化的類,然后用反序列攻擊單例
public class SerializationTest { public static void main(String[] args) throws IOException, ClassNotFoundException { Person person = Person.getInstance(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("person")); objectOutputStream.writeObject(person); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("person")); Person person1 = (Person) objectInputStream.readObject(); System.out.println(person == person1); // false } }
比較兩個 person 實例地址,是 false,說明生成了兩個對象,違背了單例類的初衷,那么為了能在序列化過程仍能保持單例的特性,可以在Person類中添加一個readResolve()方法,在該方法中直接返回Person的單例對象
public Object readResolve() { return Holder.instatnce; }
原理是當從 I/O 流中讀取對象時,ObjectInputStream 類里有 readResolve() 方法,該方法會被自動調用,期間經過種種邏輯,最后會調用到可序列化類里的 readResolve()方法,這樣可以用 readResolve() 中返回的單例對象直接替換在反序列化過程中創建的對象,實現單例特性。
也就是說,無論如何,反序列化都會額外創建對象,只不過使用 readResolve() 方法可以替換之。
具體有關Java 對象的序列化可以參考筆記:深入理解Java對象序列化
反射攻擊單例類
直接看例子,做法很簡單,通過 Java 的反射機制,看看能不能拿到單例類的私有構造器,並且改變構造器的訪問屬性
public class ReflectTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException { Person person = Person.getInstance(); Class clazz = Class.forName("com.dashuai.D13Singleton.Person"); Constructor constructor = clazz.getDeclaredConstructor(); // constructor.setAccessible(true); Person person1 = (Person) constructor.newInstance(); System.out.println(person == person1); // false } }
運行拋出了異常:
但是,如果把注釋的行打開,就不會出錯,且打印 false。
網上有一些解決方案,比如在構造器里加判斷,如果二次調用就拋出異常,其實也沒從根本上解決問題。
解決所有問題的方案——枚舉實現單例類
目前公認的最佳方案,代碼極少,線程安全,防止反射和序列化攻擊
public enum EnumSingleton { ENUM_SINGLETON; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } ////////////////////////調用 EnumSingleton.ENUM_SINGLETON.setName("dashuai"); System.out.println(EnumSingleton.ENUM_SINGLETON.getName());
所有的變量都是單例的。至於為什么,可以通過反編譯工具查看枚舉的源碼。可以安裝 idea 的 jad 插件,會發現就是按照單例模式設計的。
享元模式和單例模式的異同
享元模式是對象級別的, 也就是說在多個使用到這個對象的地方都只需要使用這一個對象即可滿足要求。
單例模式是類級別的, 就是說這個類必須只能實例化出來一個對象。
可以這么說, 單例是享元的一種特例, 設計模式不用拘泥於具體代碼, 代碼實現可能有n多種方式, 而單例可以看做是享元的實現方式中的一種, 他比享元模式更加嚴格的控制了對象的唯一性
使用單例的場景和條件是什么?
1、單例類只能有一個實例。
2、單例類必須自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。
JDK里有哪些使用了單例模式的例子?說明之
java.lang.runtime getRuntime,代碼也很簡單。
歡迎關注
dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!