前言
什麼是單例模式
單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。
這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。
這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。
這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
注意:
1、單例類只能有一個實例。
2、單例類必須自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。
# 摘自菜鳥教程:https://www.runoob.com/design-pattern/singleton-pattern.html
單例模式又分 餓漢模式,懶漢模式本片文章主要講解懶漢模式
懶漢模式
- 首先來看一下它的定義
懶漢模式:延遲加載,只有在真正使用的時候,才開始實例化.
實現方式
1.雙檢鎖
private static volatile LazySingleton instance;
private LazySingleton(){
if(instance != null){
throw new RuntimeException("不允許通過反射獲取");
}
}
public static LazySingleton getInstance() {
//第一次檢查
if (instance == null) {
//獲取鎖
//第一次訪問,多個線程同時擠進來,只有一個線程可以獲取鎖
synchronized (LazySingleton.class){
//第一個線程進入 此處為空,進入if並創建對象並返回,之後獲得鎖的線程此處判斷不為空直接返回
if(instance == null) {
//執行構造方法
instance = new LazySingleton();
}
}
}
return instance;
}
問題1:此處為什麼使用volatile.private static volatile LazySingleton instance;
創建對象是非原子性操作,有三個過程 分配空間,初始化對象,賦值,其中2,3步其中可能會發生指令重排現象.
-
代碼示例
//假設我們有如下語句 Holder holder = new Holder(); //則實際執行的操作如下 tmpRef=allocate(Holder.class)//1.分配空間 invokeConstructor(tmpRef)//2. 執行構造函數 holder = tmpRef //3.賦值
- 只有在初始化對象的那一步才會真正執行構造方法
- 編譯器(JIT),CPU有可能對指令進行重排序,導致使用到尚未初始化的實例,可以通過添加volatile關鍵字進行修飾,對於volatile修飾的字段,可以防止指令重排.
問題2:構造方法的反射判斷
對於構造方法聲明為private,可以防止直接new對象,但是阻止不了反射來獲取對象,從而破壞單例.
private LazySingleton(){
if(instance != null){
throw new RuntimeException("不允許通過反射獲取");
}
}
以上代碼並不能完美的阻止反射,如果從一開始就直接使用反射而不直接去調用提供的創建方法 就會被破解
- 代碼示例
Class<LazySingleton> lazySingletonClass = LazySingleton.class;
Constructor<LazySingleton> constructor = lazySingletonClass.getDeclaredConstructor();
// 暴力反射
constructor.setAccessible(true);
// 從一開始就不使用給定的方法來創建單例
//LazySingleton instance = LazySingleton.getInstance();
LazySingleton lazySingleton = constructor.newInstance();
LazySingleton lazySingleton1 = constructor.newInstance();
System.out.println(lazySingleton);
System.out.println(lazySingleton1);
執行結果
com.leetao.singleton.LazySingleton@511d50c0
com.leetao.singleton.LazySingleton@60e53b93
完全是兩個對象...
- 難道就真的任反射隨意宰割了? 彆著急,下面會通過靜態內部類的方式來介紹如何解決的
- 在這之前,還是先得來了解一下序列化破壞反射吧
序列化破壞
- 代碼實例
//序列化的對象已實現Serializable接口
//內存輸出流,此處也可以使用文件輸出流(持久化)來代替
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//內存輸入流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
//獲取單例
LazySingleton instance = LazySingleton.getInstance();
//存入對象輸出中
objectOutputStream.writeObject(instance);
objectOutputStream.flush();
objectOutputStream.close();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
//對象輸入流
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
LazySingleton lazySingleton = ((LazySingleton) objectInputStream.readObject());
//方法創建的單例
System.out.println(instance);
//序列化之後的單例
System.out.println(lazySingleton);
運行結果
com.leetao.singleton.LazySingleton@6f94fa3e
com.leetao.singleton.LazySingleton@4e50df2e
也不是同一個...
- 關於序列化對象之後為什麼不是同一個的問題
- 因為使用了默認的序列化機制,他會直接從字節流中拿數據,並不會去調構造函數來進行初始化
附1:JAVA序列化過程
1.將對象實例相關的類元數據輸出。
2.遞歸地輸出類的超類描述直到不再有超類。
3.類元數據完了以後,開始從最頂層的超類開始輸出對象實例的實際數據值。
4.從上至下遞歸輸出實例的數據
腳註: 默認的序列化機制會將對象所有實現Serializable接口的-內容-全部序列化,序列化過程會讀取內容的字節流數據,會通過此產生新的對象,並不是通過構造函數來製造新的對象(網上很多文章都說序列化通過構造函數來創建對象,其實並不是!!)。
原型模式中的深克隆也就是通過此機制來實現的.
腳註所示內容代表的是:對象繼承的類以及超類..、成員變量中的引用變量
-
那麼單例中的序列化破壞如何解決
官方答案
- 可以通過寫入readResovler() throws ObjectStreamException 方法來實現自定義的序列化機制
代碼
/**
* 自定義的序列化機制
*/
private Object readResolve() throws ObjectStreamException{
//直接返回單例
return LazySingleton.getInstance();
}
再次運行結果
com.leetao.singleton.LazySingleton@6f94fa3e
com.leetao.singleton.LazySingleton@6f94fa3e
序列化破壞的問題完美解決
2. 靜態內部類
1.本質上是利用類加載器機制來保證線程安全
2.只有在實際使用的時候,才會觸發類的初始化,所以也是懶加載的一種形式
3.藉助於jvm類加載機制,保證實例的唯一性.
何為類加載
# 類加載過程
1. 加載: 將字節碼數據從不同的數據源讀取到 JVM 中,並映射為 JVM 認可的數據結構(Class 對象)
2. 連接: a.校驗,b.準備(給類的靜態成員變量賦默認值),c.解析
3. 初始化:給類的靜態變量賦初值
# 注意:只有在真正使用對應的類是,才會觸發初始化 如
1. 當前類是啟動類(main方法所在的類).
2. 直接進行new操作.
3. 訪問靜態字段(final修飾的靜態字面量除外).
4. 訪問靜態方法.
5. 用反射訪問類.
6. 初始化此類的子類.
$. 後續會出關於類加載相關的文章
前面提到反射破壞的問題,在靜態內部類中可以這樣解決
public class LeeFactory {
private LeeFactory(){
//通過靜態類加載機制破解反射破壞
if(LeeFactoryHolder.LEE_FACTORY!=null){
throw new UnsupportedOperationException("非法反射不予允許");
}
}
/**
* 靜態內部類
*/
private static class LeeFactoryHolder{
private static final LeeFactory LEE_FACTORY = new LeeFactory();
}
public static LeeFactory getInstance(){
return LeeFactoryHolder.LEE_FACTORY;
}
}
以上代碼可以完美解決反射破壞,如果直接通過getInstance()的方式來獲取對象的話.第一次調用才會觸發類初始化和構造方法.之後的調用直接拿數據
而第一次調用反射會觸發兩次兩次構造方法,
1.構造方法中if中的判斷會調用一次(因為訪問類字段會涉及到類初始化,類初始化調用了構造方法)
2.反射創建對象本身會調用一次構造方法,此時靜態內部類字段因為初始化已經存在值了(判斷有值,拋出異常)
還有一個特點
private static final LeeFactory LEE_FACTORY = new LeeFactory();
為什麼加final
1.因為一旦被賦值便無法在修改,即使是反射也不能(也是因為這個原因才使用final)
2.被final修飾的字段會在完全初始化後才會對其他線程可見
說到這裡不得不說為什麼不用volatile
final和volatile
volatile修飾的字段不但可以防止重排序,還可以直接在主存更新,讓其他線程同步更新
但是它阻止不了反射對其重新賦值,如果使用反射對內部類字段賦值為null,會導致其他正常調用的代碼出現問題.
而final修飾的類,會在完全初始化後才會對其他線程可見,而且不能被反射破壞,正好符合我們的需求
如果對final感興趣的同學,可以閱讀
https://zhuanlan.zhihu.com/p/100536345 瞭解更多
結尾:淺談枚舉單例
枚舉本質上是一個不可變類 ,它的成員全部為類字段.它不可以被反射所破壞,同時還擁有自己的序列化機制.可以說是完美的單例.
參考
Java序列化機制https://www.iteye.com/blog/bingobird-867950
final特徵 https://zhuanlan.zhihu.com/p/100536345
文中所述內容,如有錯誤,歡迎指正.
忌妒別人,不會給自己增加任何的好處,忌妒別人,也不可能減少別人的成就。
菅江暉