深入探討單例模式
最近學習了一下單例模式,看bilibili up主“狂神說Java”講完後,發現大部分部落格都少了一個很有趣的環節,不分享出來實在是太可惜了,原視訊 https://www.bilibili.com/video/BV1K54y197iS
1、瞭解單例
這個部分小部分我相信很多部落格都講的很好,我就儘量精簡了
- 注意:
- 單例類只能有一個例項
- 這個例項由自己建立
- 這個例項必須提供給外界
- 關鍵:構造器私有化
- 建立方法:
- 餓漢式
- 懶漢式
總結:我認為建立方法可以歸根於兩種,一種是餓漢式,我在類的載入的時候就建立;還有一種懶漢式,只有在我需要的時候才去建立
2、思路及實現
【餓漢模式最基本的實現】
在類載入的時候就已經建立了,這個模式下,執行緒是安全的,不同的執行緒拿到的都是同一個例項,但是,這個也存在空間浪費的問題,我不需要的時候你也載入了。
//餓漢模式 public class HungerSingle { private static HungerSingle single = new HungerSingle(); //構造器私有,外界不能通過構造方法new物件,保證唯一 private HungerSingle() { } //提供外界獲得該單例的方法,注意方法只能是static方法,因為沒有類例項 public static HungerSingle getInstance(){ return single; } }
【懶漢模式最基本的實現】
為了解決上述那個空間浪費問題,這時候懶漢模式就起作用了,你需要我的時候我再去建立這個例項
//懶漢模式 public class LazySingle { private static LazySingle single; //構造器私有化,禁止外部new生成物件 private LazySingle(){ } //外界獲得該單例的方法 public static LazySingle getInstance(){ if(single == null){ single = new LazySingle(); } return single; } }
一位熱心前輩的評論:“像你這樣寫單例,在我們公司是要被開除的。”
趁我還是學生,懷著以後不被開除的心情,繼續學習下去
原來懶漢模式下,單例執行緒是不安全的。
怎麼測試呢?如下
【測試懶漢模式執行緒不安全】
//1、構造器
private LazySingle(){
System.out.println(Thread.currentThread().getName());
}
//建立十個執行緒
for (int i = 0; i < 10; i++) {
new Thread(()->{
Singleton2.getInstance();
}).start();
}
此時你會發現,構造方法呼叫了不止一次,說明沒有實現預期的單例
平時我們解決執行緒不安全的方法:不就是執行緒不安全嘛,那好辦,加鎖
【雙重檢測鎖/DCL】
public class DCLSingle {
private static DCLSingle single;
private DCLSingle(){
}
public static DCLSingle getInstance(){
//第一次判斷,沒有這個物件才加鎖
if(single == null){
//哪個需要保護,就鎖哪個
synchronized (DCLSingle.class){
//第二次判斷,沒有就例項化
if(single == null){
single = new DCLSingle();
}
}
}
return single;
}
}
仔細和別人程式碼一比對,發現我少了個volatile關鍵字,這是啥玩意?
不懂就問。
【volatile】
為了避免指令重排
//上述程式碼宣告上面加上volatile關鍵字
private volatile static DCLSingle single;
啥是volatile ?
引用自別人部落格
https://www.cnblogs.com/YLsY/p/11295732.html
加volatile是為了出現髒讀的出現,保證操作的原子性
1、原子性操作:不可再分割的操作
例如:single = new DCLSingle();
其實就是兩步操作:
①new DCLSingle();//開闢堆記憶體
②singl指向對記憶體
2、髒讀
Java記憶體模型規定所有的變數都是存在主存當中,每個執行緒都有自己的工作記憶體。
執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接對主存進行操作。
並且每個執行緒不能訪問其他執行緒的工作記憶體。
變數的值何時從執行緒的工作記憶體寫回主存,無法確定。
3、指令重排
single = new DCLSingle();
先執行②
後執行①
//先指向堆記憶體,還未完成構造
【模擬情況】
①執行緒1執行,在自己的工作記憶體定義引用,先指向堆記憶體,還未構造完成
②此時執行緒2執行,它進行判斷,引用已經指向了記憶體,所以執行緒2,認為構造完成,實際還未構造完成
還有一種差點忘記說了,也是菜鳥教程說建議使用的方式
【靜態內部類實現單例】
public class Singleton {
private Singleton(){}
private static class SingleIN{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton getInstance(){
return SingleIN.INSTANCE;
}
}
你會發現它和前面講的普通餓漢式很像,我把它也歸於餓漢式一類,因為它也是直接就new Singleton,但是它卻有著懶載入的效果,而這種方式是 Singleton 類被裝載了,instance 不一定被初始化。因為 SingletonHolder 類沒有被主動使用,只有通過顯式呼叫 getInstance 方法時,才會顯式裝載 SingletonHolder 類,從而例項化 instance。
【建議】建議使用靜態內部類實現
## 3、如何破化單例(其它大部分部落格沒有的內容) 在這裡感謝b站up【狂神說java】
在面試官面前裝逼的時候來了
java語言實現動態化的靈魂——反射,說:沒有什麼是我不能改變的,看我來如何操作。
【反射破壞單例】
public class DCLSingle {
private static DCLSingle single;
private DCLSingle(){
}
public static DCLSingle getInstance(){
//第一次判斷,沒有這個物件才加鎖
if(single == null){
//哪個需要保護,就鎖哪個
synchronized (DCLSingle.class){
//第二次判斷,沒有就例項化
if(single == null){
single = new DCLSingle();
}
}
}
return single;
}
//通過反射破化單例
public static void main(String[] args) throws Exception {
LazySingle single = LazySingle.getInstance();
Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingle single1 = constructor.newInstance();
System.out.println(single == single1);//false
}
}
得到單例類的構造器,然後通過newInstance的方法建立物件,很明顯破化了單例
【改進程式碼,防止你搞破化】
既然這次你是通過得到構造器破化的,那我給構造器加個方法,如果你已經建立了例項,那就丟擲異常
private LazySingle(){
synchronized(LazySingle.class){
if(single!=null){
throw new RuntimeException("破壞失敗");
}
}
}
但是這個又有問題,這裡的判斷是private static DCLSingle single 是否有值,如果我們都不通過getInstance()方法建立物件,而是這樣
public static void main(String[] args) throws Exception {
// LazySingle single = LazySingle.getInstance();
Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
//注意:這裡的物件不是單例類中裡面屬性的那個物件
LazySingle single = constructor.newInstance();
LazySingle single1 = constructor.newInstance();
System.out.println(single == single1);//false
}
這裡根本不會丟擲異常,而是又破壞了單例
【繼續改進程式碼,防止搞破化】
簡直就是相愛相殺呀,我們可以利用紅路燈原理,防止破化
改進構造方法
//加個標誌
private static String sign = "password";
private LazySingle(){
synchronized(LazySingle.class){
if(single!=null || !"password".equals(sign)){
throw new RuntimeException("破壞失敗");
}else{
sign = "no";
}
}
}
此刻你通過上述main()方法裡面的內容測試,發現又會丟擲異常。然而我們能通過反射獲得構造方法,那我們同樣也能通過反射獲取物件的屬性以及值吧
【再度破化】
public static void main(String[] args) throws Exception {
Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
Field field = LazySingle.class.getDeclaredField("sign");
//此處省略通過反射獲取該屬性的型別和方法....
LazySingle single1 = constructor.newInstance();
//重新變回原標誌位
field.set("sign","password");
LazySingle single2 = constructor.newInstance();
System.out.println(single2 == single1);//false
}
又被破化了
【再次改進】
我們將目光拋向列舉,
jdk1.5之後,出現列舉
利用列舉實現不僅能避免多執行緒同步問題,而且還自動支援序列化機制,防止反序列化重新建立新的物件,絕對防止多次例項化(菜鳥教程官方術語)
public enum Singleton {
INSTANCE;
public Singleton getInstance() {
return INSTANCE
}
}
【反射能破化列舉的單例嗎?】
- 我們先要了解列舉是啥,它的底層是怎麼實現的
- 我們會發現列舉本身就是一個
- 通過反編譯工具,檢視列舉底層的構造方法
- 通過反射獲取構造方
- 重複上述測試
我們最終可以發現反射不能破化列舉的單例
這種實現方式還沒有被廣泛採用,但這是實現單例模式的最佳方法。它更簡潔,自動支援序列化機制,絕對防止多次例項化。(菜鳥教程官方)
【總結】太難