溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何使用單例模式

發(fā)布時間:2021-10-19 09:27:45 來源:億速云 閱讀:140 作者:iii 欄目:web開發(fā)

這篇文章主要講解了“如何使用單例模式”,文中的講解內(nèi)容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何使用單例模式”吧!

餓漢式

餓漢式是最常見的也是最不需要考慮太多的單例模式,因為他不存在線程安全問題,餓漢式也就是在類被加載的時候就創(chuàng)建實例對象。餓漢式的寫法如下:

public class SingletonHungry {     private static SingletonHungry instance = new SingletonHungry();      private SingletonHungry() {     }      private static SingletonHungry getInstance() {         return instance;     } }
  • 測試代碼如下:

class A {     public static void main(String[] args) {         IntStream.rangeClosed(1, 5)                 .forEach(i -> {                     new Thread(                             () -> {                                 SingletonHungry instance = SingletonHungry.getInstance();                                 System.out.println("instance = " + instance);                             }                     ).start();                 });     } }

結(jié)果

如何使用單例模式

優(yōu)點:線程安全,不需要關(guān)心并發(fā)問題,寫法也是最簡單的。

缺點:在類被加載的時候?qū)ο缶蜁粍?chuàng)建,也就是說不管你是不是用到該對象,此對象都會被創(chuàng)建,浪費內(nèi)存空間

懶漢式

以下是最基本的餓漢式的寫法,在單線程情況下,這種方式是非常完美的,但是我們實際程序執(zhí)行基本都不可能是單線程的,所以這種寫法必定會存在線程安全問題

public class SingletonLazy {     private SingletonLazy() {     }      private static SingletonLazy instance = null;      public static SingletonLazy getInstance() {         if (null == instance) {             return new SingletonLazy();         }         return instance;      } }

演示多線程執(zhí)行

class B {     public static void main(String[] args) {         IntStream.rangeClosed(1, 5)                 .forEach(i -> {                     new Thread(                             () -> {                                 SingletonLazy instance = SingletonLazy.getInstance();                                 System.out.println("instance = " + instance);                             }                     ).start();                 });     } }

結(jié)果

如何使用單例模式

結(jié)果很顯然,獲取的實例對象不是單例的。也就是說這種寫法不是線程安全的,也就不能在多線程情況下使用

DCL(雙重檢查鎖式)

DCL 即 Double Check Lock  就是在創(chuàng)建實例的時候進行雙重檢查,首先檢查實例對象是否為空,如果不為空將當前類上鎖,然后再判斷一次該實例是否為空,如果仍然為空就創(chuàng)建該是實例;代碼如下:

public class SingleTonDcl {     private SingleTonDcl() {     }      private static SingleTonDcl instance = null;      public static SingleTonDcl getInstance() {         if (null == instance) {             synchronized (SingleTonDcl.class) {                 if (null == instance) {                     instance = new SingleTonDcl();                 }             }         }         return instance;     } }

測試代碼如下:

class C {     public static void main(String[] args) {         IntStream.rangeClosed(1, 5)                 .forEach(i -> {                     new Thread(                             () -> {                                 SingleTonDcl instance = SingleTonDcl.getInstance();                                 System.out.println("instance = " + instance);                             }                     ).start();                 });     } }

結(jié)果

如何使用單例模式

相信大多數(shù)初學者在接觸到這種寫法的時候已經(jīng)感覺是「高大上」了,首先是判斷實例對象是否為空,如果為空那么就將該對象的 Class  作為鎖,這樣保證同一時刻只能有一個線程進行訪問,然后再次判斷實例對象是否為空,最后才會真正的去初始化創(chuàng)建該實例對象。一切看起來似乎已經(jīng)沒有破綻,但是當你學過JVM后你可能就會一眼看出貓膩了。沒錯,問題就在  instance = new SingleTonDcl(); 因為這不是一個原子的操作,這句話的執(zhí)行是在 JVM 層面分以下三步:

1.給 SingleTonDcl 分配內(nèi)存空間 2.初始化 SingleTonDcl 實例 3.將 instance 對象指向分配的內(nèi)存空間(  instance 為 null 了)

正常情況下上面三步是順序執(zhí)行的,但是實際上JVM可能會「自作多情」得將我們的代碼進行優(yōu)化,可能執(zhí)行的順序是1、3、2,如下代碼所示

public static SingleTonDcl getInstance() {     if (null == instance) {         synchronized (SingleTonDcl.class) {             if (null == instance) {                 1. 給 SingleTonDcl 分配內(nèi)存空間                 3.將 instance 對象指向分配的內(nèi)存空間( instance 不為 null 了)                 2. 初始化 SingleTonDcl 實例             }         }     }     return instance; }

假設(shè)現(xiàn)在有兩個線程 t1, t2

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 如果 t1 執(zhí)行到以上步驟 3 被掛起

  3. 然后 t2 進入了 getInstance 方法,由于 t1 執(zhí)行了步驟 3,此時的 instance 已經(jīng)不為空了,所以 if (null ==  instance) 這個條件不為空,直接返回 instance, 但由于 t1 還未執(zhí)行步驟 2,導致此時的 instance  實際上是個半成品,會導致不可預知的風險!

該怎么解決呢,既然問題出在指令有可能重排序上,不讓它重排序不就行了,volatile 不就是干這事的嗎,我們可以在 instance 變量前面加上一個  volatile 修飾符

畫外音:volatile 的作用 1.保證的對象內(nèi)存可見性 2.防止指令重排序

優(yōu)化后的代碼如下

public class SingleTonDcl {     private SingleTonDcl() {     }      //在對象前面添加 volatile 關(guān)鍵字即可     volatile private static SingleTonDcl instance = null;      public static SingleTonDcl getInstance() {         if (null == instance) {             synchronized (SingleTonDcl.class) {                 if (null == instance) {                     instance = new SingleTonDcl();                 }             }         }         return instance;     } }

到這里似乎問題已經(jīng)解決了,雙重鎖機制 + volatile 實際上確實基本上解決了線程安全問題,保證了“真正”的單例。但真的是這樣的嗎?繼續(xù)往下看

靜態(tài)內(nèi)部類

先看代碼

public class SingleTonStaticInnerClass {     private SingleTonStaticInnerClass() {      }      private static class HandlerInstance {         private static SingleTonStaticInnerClass instance = new SingleTonStaticInnerClass();     }      public static SingleTonStaticInnerClass getInstance() {         return HandlerInstance.instance;     } }
  • 測試代碼如下:

class D {     public static void main(String[] args) {         IntStream.rangeClosed(1, 5)                 .forEach(i->{                     new Thread(()->{                         SingleTonStaticInnerClass instance = SingleTonStaticInnerClass.getInstance();                         System.out.println("instance = " + instance);                     }).start();                 });     } }

如何使用單例模式

靜態(tài)內(nèi)部類的特點:

這種寫法使用 JVM 類加載機制保證了線程安全問題;由于 SingleTonStaticInnerClass 是私有的,除了 getInstance()  之外沒有辦法訪問它,因此它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本;

但是,它依舊不是完美的。

不安全的單例

上面實現(xiàn)單例都不是完美的,主要有兩個原因

1. 反射攻擊

首先要提到 java 中讓人又愛又恨的反射機制, 閑言少敘,我們直接邊上代碼邊說明,這里就以 DCL 舉例(為什么選擇 DCL 因為很多人覺得 DCL  寫法是最高大上的....這里就開始去”打他們的臉“)

將上面的 DCl 的測試代碼修改如下:

class C {     public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {         Class<SingleTonDcl> singleTonDclClass = SingleTonDcl.class;         //獲取類的構(gòu)造器         Constructor<SingleTonDcl> constructor = singleTonDclClass.getDeclaredConstructor();         //把構(gòu)造器私有權(quán)限放開         constructor.setAccessible(true);         //反射創(chuàng)建實例   注意反射創(chuàng)建要放在前面,才會攻擊成功,因為如果反射攻擊在后面,先使用正常的方式創(chuàng)建實例的話,在構(gòu)造器中判斷是可以防止反射攻擊、拋出異常的,         //因為先使用正常的方式已經(jīng)創(chuàng)建了實例,會進入if         SingleTonDcl instance = constructor.newInstance();         //正常的獲取實例方式   正常的方式放在反射創(chuàng)建實例后面,這樣當反射創(chuàng)建成功后,單例對象中的引用其實還是空的,反射攻擊才能成功         SingleTonDcl instance1 = SingleTonDcl.getInstance();         System.out.println("instance1 = " + instance1);         System.out.println("instance = " + instance);     } }

如何使用單例模式

居然是兩個對象!內(nèi)心是不是異常平靜?果然和你想的不一樣?其他的方式基本類似,都可以通過反射破壞單例。

2. 序列化攻擊

我們以「餓漢式單例」為例來演示一下序列化和反序列化攻擊代碼,首先給餓漢式單例對應的類添加實現(xiàn) Serializable 接口的代碼,

public class SingletonHungry implements Serializable {     private static SingletonHungry instance = new SingletonHungry();      private SingletonHungry() {     }      private static SingletonHungry getInstance() {         return instance;     } }

然后看看如何使用序列化和反序列化進行攻擊

SingletonHungry instance = SingletonHungry.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"))); // 序列化【寫】操作 oos.writeObject(instance); File file = new File("singleton_file"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)) // 反序列化【讀】操作 SingletonHungry newInstance = (SingletonHungry) ois.readObject(); System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance);

來看下結(jié)果圖片

如何使用單例模式

果然出現(xiàn)了兩個不同的對象!這種反序列化攻擊其實解決方式也簡單,重寫反序列化時要調(diào)用的 readObject 方法即可

private Object readResolve(){     return instance; }

這樣在反序列化時候永遠只讀取 instance 這一個實例,保證了單例的實現(xiàn)。

真正安全的單例: 枚舉方式

public enum SingleTonEnum {     /**      * 實例對象      */     INSTANCE;     public void doSomething() {         System.out.println("doSomething");     } }

調(diào)用方法

public class Main {     public static void main(String[] args) {         SingleTonEnum.INSTANCE.doSomething();     } }

枚舉模式實現(xiàn)的單例才是真正的單例模式,是完美的實現(xiàn)方式

有人可能會提出疑問:枚舉是不是也能通過反射來破壞其單例實現(xiàn)呢?

試試唄,修改枚舉的測試類

class E{     public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {         Class<SingleTonEnum> singleTonEnumClass = SingleTonEnum.class;         Constructor<SingleTonEnum> declaredConstructor = singleTonEnumClass.getDeclaredConstructor();         declaredConstructor.setAccessible(true);         SingleTonEnum singleTonEnum = declaredConstructor.newInstance();         SingleTonEnum instance = SingleTonEnum.INSTANCE;         System.out.println("instance = " + instance);         System.out.println("singleTonEnum = " + singleTonEnum);     } }

結(jié)果

如何使用單例模式

沒有無參構(gòu)造?我們使用 javap 工具來查下字節(jié)碼看看有啥玄機

如何使用單例模式

好家伙,發(fā)現(xiàn)一個有參構(gòu)造器 String Int ,那就試試唄

//獲取構(gòu)造器的時候修改成這樣子 Constructor<SingleTonEnum> declaredConstructor = singleTonEnumClass.getDeclaredConstructor(String.class,int.class);

結(jié)果

如何使用單例模式

好家伙,拋出了異常,異常信息寫著: 「Cannot reflectively create enum objects」

源碼之下無秘密,我們來看看 newInstance() 到底做了什么?為啥用反射創(chuàng)建枚舉會拋出這么個異常?

如何使用單例模式

真相大白!如果是枚舉,不允許通過反射來創(chuàng)建,這才是使用 enum 創(chuàng)建單例才可以說是真正安全的原因!

感謝各位的閱讀,以上就是“如何使用單例模式”的內(nèi)容了,經(jīng)過本文的學習后,相信大家對如何使用單例模式這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI