前言
什麼是單例模式
單例模式(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-867950final特征
文中所述内容,如有錯誤,歡迎指正.
忌妒别人,不會給自己增加任何的好處,忌妒别人,也不可能減少别人的成就。
菅江晖