成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

我用 Dcl寫出了單例模式,結果阿里面試官不滿意!

開發 前端
單例模式可以說是設計模式中最簡單和最基礎的一種設計模式了,哪怕是一個初級開發,在被問到使用過哪些設計模式的時候,估計多數會說單例模式。

[[381908]]

前言

單例模式可以說是設計模式中最簡單和最基礎的一種設計模式了,哪怕是一個初級開發,在被問到使用過哪些設計模式的時候,估計多數會說單例模式。但是你認為這么基本的”單例模式“真的就那么簡單嗎?或許你會反問:「一個簡單的單例模式該是咋樣的?」哈哈,話不多說,讓我們一起拭目以待,堅持看完,相信你一定會有收獲!

餓漢式

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

  1. public class SingletonHungry { 
  2.     private static SingletonHungry instance = new SingletonHungry(); 
  3.  
  4.     private SingletonHungry() { 
  5.     } 
  6.  
  7.     private static SingletonHungry getInstance() { 
  8.         return instance; 
  9.     } 
  • 測試代碼如下:
  1. class A { 
  2.     public static void main(String[] args) { 
  3.         IntStream.rangeClosed(1, 5) 
  4.                 .forEach(i -> { 
  5.                     new Thread( 
  6.                             () -> { 
  7.                                 SingletonHungry instance = SingletonHungry.getInstance(); 
  8.                                 System.out.println("instance = " + instance); 
  9.                             } 
  10.                     ).start(); 
  11.                 }); 
  12.     } 

結果

優點:線程安全,不需要關心并發問題,寫法也是最簡單的。

缺點:在類被加載的時候對象就會被創建,也就是說不管你是不是用到該對象,此對象都會被創建,浪費內存空間

懶漢式

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

  1. public class SingletonLazy { 
  2.     private SingletonLazy() { 
  3.     } 
  4.  
  5.     private static SingletonLazy instance = null
  6.  
  7.     public static SingletonLazy getInstance() { 
  8.         if (null == instance) { 
  9.             return new SingletonLazy(); 
  10.         } 
  11.         return instance; 
  12.  
  13.     } 

演示多線程執行

  1. class B { 
  2.     public static void main(String[] args) { 
  3.         IntStream.rangeClosed(1, 5) 
  4.                 .forEach(i -> { 
  5.                     new Thread( 
  6.                             () -> { 
  7.                                 SingletonLazy instance = SingletonLazy.getInstance(); 
  8.                                 System.out.println("instance = " + instance); 
  9.                             } 
  10.                     ).start(); 
  11.                 }); 
  12.     } 

結果

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

DCL(雙重檢查鎖式)

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

  1. public class SingleTonDcl { 
  2.     private SingleTonDcl() { 
  3.     } 
  4.  
  5.     private static SingleTonDcl instance = null
  6.  
  7.     public static SingleTonDcl getInstance() { 
  8.         if (null == instance) { 
  9.             synchronized (SingleTonDcl.class) { 
  10.                 if (null == instance) { 
  11.                     instance = new SingleTonDcl(); 
  12.                 } 
  13.             } 
  14.         } 
  15.         return instance; 
  16.     } 

測試代碼如下:

  1. class C { 
  2.     public static void main(String[] args) { 
  3.         IntStream.rangeClosed(1, 5) 
  4.                 .forEach(i -> { 
  5.                     new Thread( 
  6.                             () -> { 
  7.                                 SingleTonDcl instance = SingleTonDcl.getInstance(); 
  8.                                 System.out.println("instance = " + instance); 
  9.                             } 
  10.                     ).start(); 
  11.                 }); 
  12.     } 

結果

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

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

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

  1. public static SingleTonDcl getInstance() { 
  2.     if (null == instance) { 
  3.         synchronized (SingleTonDcl.class) { 
  4.             if (null == instance) { 
  5.                 1. 給 SingleTonDcl 分配內存空間 
  6.                 3.將 instance 對象指向分配的內存空間( instance 不為 null 了) 
  7.                 2. 初始化 SingleTonDcl 實例 
  8.             } 
  9.         } 
  10.     } 
  11.     return instance; 

假設現在有兩個線程 t1, t2

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

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

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

優化后的代碼如下

  1. public class SingleTonDcl { 
  2.     private SingleTonDcl() { 
  3.     } 
  4.  
  5.     //在對象前面添加 volatile 關鍵字即可 
  6.     volatile private static SingleTonDcl instance = null
  7.  
  8.     public static SingleTonDcl getInstance() { 
  9.         if (null == instance) { 
  10.             synchronized (SingleTonDcl.class) { 
  11.                 if (null == instance) { 
  12.                     instance = new SingleTonDcl(); 
  13.                 } 
  14.             } 
  15.         } 
  16.         return instance; 
  17.     } 

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

靜態內部類

先看代碼

  1. public class SingleTonStaticInnerClass { 
  2.     private SingleTonStaticInnerClass() { 
  3.  
  4.     } 
  5.  
  6.     private static class HandlerInstance { 
  7.         private static SingleTonStaticInnerClass instance = new SingleTonStaticInnerClass(); 
  8.     } 
  9.  
  10.     public static SingleTonStaticInnerClass getInstance() { 
  11.         return HandlerInstance.instance; 
  12.     } 
  • 測試代碼如下:
  1. class D { 
  2.     public static void main(String[] args) { 
  3.         IntStream.rangeClosed(1, 5) 
  4.                 .forEach(i->{ 
  5.                     new Thread(()->{ 
  6.                         SingleTonStaticInnerClass instance = SingleTonStaticInnerClass.getInstance(); 
  7.                         System.out.println("instance = " + instance); 
  8.                     }).start(); 
  9.                 }); 
  10.     } 

靜態內部類的特點:

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

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

不安全的單例

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

1. 反射攻擊

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

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

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

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

2. 序列化攻擊

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

  1. public class SingletonHungry implements Serializable { 
  2.     private static SingletonHungry instance = new SingletonHungry(); 
  3.  
  4.     private SingletonHungry() { 
  5.     } 
  6.  
  7.     private static SingletonHungry getInstance() { 
  8.         return instance; 
  9.     } 

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

  1. SingletonHungry instance = SingletonHungry.getInstance(); 
  2. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"))); 
  3. // 序列化【寫】操作 
  4. oos.writeObject(instance); 
  5. File file = new File("singleton_file"); 
  6. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)) 
  7. // 反序列化【讀】操作 
  8. SingletonHungry newInstance = (SingletonHungry) ois.readObject(); 
  9. System.out.println(instance); 
  10. System.out.println(newInstance); 
  11. System.out.println(instance == newInstance); 

來看下結果圖片

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

  1. private Object readResolve(){ 
  2.     return instance; 

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

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

  1. public enum SingleTonEnum { 
  2.     /** 
  3.      * 實例對象 
  4.      */ 
  5.     INSTANCE; 
  6.     public void doSomething() { 
  7.         System.out.println("doSomething"); 
  8.     } 

調用方法

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

枚舉模式實現的單例才是真正的單例模式,是完美的實現方式

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

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

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

結果

沒有無參構造?我們使用 javap 工具來查下字節碼看看有啥玄機

好家伙,發現一個有參構造器 String Int ,那就試試唄

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

結果

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

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

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

結束語

以上就是一些關于單例模式的知識點匯總,你還真不要小看這個小小的單例,面試的時候多數候選人寫不對這么一個簡單的單例,寫對的多數也僅止于 DCL,但再問是否有啥不安全,如何用 enum 寫出安全的單例時,幾乎沒有人能答出來!有人說能寫出 DCL 就行了,何必這么鉆牛角尖?但我想說的是正是這種鉆牛角尖的精神能讓你逐步積累技術深度,成為專家,對技術有一探究竟的執著,何愁成不了專家?

本文轉載自微信公眾號「碼海」,可以通過以下二維碼關注。轉載本文請聯系碼海公眾號。

 

責任編輯:武曉燕 來源: 碼海
相關推薦

2020-07-20 07:48:53

單例模式

2010-10-11 09:33:15

微軟鮑爾默

2024-08-13 17:56:52

單例裝飾器模式

2021-11-02 22:04:58

模式

2020-08-03 07:38:12

單例模式

2016-04-25 15:43:06

戴爾存儲電商

2020-08-13 10:15:34

MySQL數據庫面試

2012-03-20 13:54:06

滿意率

2020-02-25 16:56:02

面試官有話想說

2021-09-27 07:11:18

MySQLACID特性

2022-11-15 17:45:46

數據庫MySQL

2020-07-27 07:27:03

程序員技術編碼

2021-03-02 08:50:31

設計單例模式

2021-11-05 10:07:13

Redis哈希表存儲

2021-01-20 07:16:07

冪等性接口token

2021-12-02 08:19:06

MVCC面試數據庫

2021-07-27 07:31:16

單例模式關鍵字

2023-11-27 08:32:02

元素HashMap

2019-11-21 08:40:44

面試官優化性能

2009-04-07 08:48:26

微軟Windows 7操作系統
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 精品视频免费在线 | 久久综合成人精品亚洲另类欧美 | 伊人免费观看视频 | 欧美不卡一区 | 在线观看欧美日韩视频 | 三区在线| 国产精品久久久久无码av | 久久综合九色综合欧美狠狠 | 色婷婷综合久久久中文字幕 | 国产在线播放av | av手机在线免费观看 | 国产草草视频 | 一区二区三区在线 | 欧美综合一区二区三区 | 中文字幕日本一区二区 | 日韩在线看片 | 91精品国产色综合久久不卡蜜臀 | 亚洲精品一区二区 | 成人特级毛片 | 久久机热| 日韩欧美久久精品 | 亚洲精品成人 | 国产亚洲一区二区精品 | 97人人超碰 | 在线观看国产wwwa级羞羞视频 | 婷婷成人在线 | 国产精品久久久久久久免费观看 | 天天综合久久 | 青青草网站在线观看 | 成人a免费| 免费不卡视频 | 天天射天天干 | 国产 日韩 欧美 在线 | 在线一级片| 一区二区三区亚洲 | www.欧美视频 | 特黄视频| 韩日精品一区 | 日韩欧美国产一区二区三区 | 精品视频在线一区 | 久久在线 |