自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

我向面試官講解了單例模式,他對(duì)我豎起了大拇指

存儲(chǔ) 存儲(chǔ)軟件
單例模式相信大家都有所聽聞,甚至也寫過不少了,在面試中也是考得最多的其中一個(gè)設(shè)計(jì)模式,面試官常常會(huì)要求寫出兩種類型的單例模式并且解釋其原理,廢話不多說,我們開始學(xué)習(xí)如何很好地回答這一道面試題吧。

 [[334211]]

單例模式相信大家都有所聽聞,甚至也寫過不少了,在面試中也是考得最多的其中一個(gè)設(shè)計(jì)模式,面試官常常會(huì)要求寫出兩種類型的單例模式并且解釋其原理,廢話不多說,我們開始學(xué)習(xí)如何很好地回答這一道面試題吧。

什么是單例模式

面試官問什么是單例模式時(shí),千萬不要答非所問,給出單例模式有兩種類型之類的回答,要圍繞單例模式的定義去展開。

單例模式是指在內(nèi)存中只會(huì)創(chuàng)建且僅創(chuàng)建一次對(duì)象的設(shè)計(jì)模式。在程序中多次使用同一個(gè)對(duì)象且作用相同時(shí),為了防止頻繁地創(chuàng)建對(duì)象使得內(nèi)存飆升,單例模式可以讓程序僅在內(nèi)存中創(chuàng)建一個(gè)對(duì)象,讓所有需要調(diào)用的地方都共享這一單例對(duì)象。

單例模式的類型

單例模式有兩種類型:

  • 懶漢式:在真正需要使用對(duì)象時(shí)才去創(chuàng)建該單例類對(duì)象
  • 餓漢式:在類加載時(shí)已經(jīng)創(chuàng)建好該單例對(duì)象,等待被程序使用

懶漢式創(chuàng)建單例對(duì)象

懶漢式創(chuàng)建對(duì)象的方法是在程序使用對(duì)象前,先判斷該對(duì)象是否已經(jīng)實(shí)例化(判空),若已實(shí)例化直接返回該類對(duì)象。否則則先執(zhí)行實(shí)例化操作。

根據(jù)上面的流程圖,就可以寫出下面的這段代碼

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

沒錯(cuò),這里我們已經(jīng)寫出了一個(gè)很不錯(cuò)的單例模式,不過它不是完美的,但是這并不影響我們使用這個(gè)“單例對(duì)象”。

以上就是懶漢式創(chuàng)建單例對(duì)象的方法,我會(huì)在后面解釋這段代碼在哪里可以優(yōu)化,存在什么問題。

餓漢式創(chuàng)建單例對(duì)象

餓漢式在類加載時(shí)已經(jīng)創(chuàng)建好該對(duì)象,在程序調(diào)用時(shí)直接返回該單例對(duì)象即可,即我們在編碼時(shí)就已經(jīng)指明了要馬上創(chuàng)建這個(gè)對(duì)象,不需要等到被調(diào)用時(shí)再去創(chuàng)建。

關(guān)于類加載,涉及到JVM的內(nèi)容,我們目前可以簡單認(rèn)為在程序啟動(dòng)時(shí),這個(gè)單例對(duì)象就已經(jīng)創(chuàng)建好了。

  1. public class Singleton{ 
  2.      
  3.     private static final Singleton singleton = new Singleton(); 
  4.      
  5.     private Singleton(){} 
  6.      
  7.     public static Singleton getInstance() { 
  8.         return singleton; 
  9.     } 

注意上面的代碼在第3行已經(jīng)實(shí)例化好了一個(gè)Singleton對(duì)象在內(nèi)存中,不會(huì)有多個(gè)Singleton對(duì)象實(shí)例存在

類在加載時(shí)會(huì)在堆內(nèi)存中創(chuàng)建一個(gè)Singleton對(duì)象,當(dāng)類被卸載時(shí),Singleton對(duì)象也隨之消亡了。

懶漢式如何保證只創(chuàng)建一個(gè)對(duì)象

我們再來回顧懶漢式的核心方法

  1. public static Singleton getInstance() { 
  2.     if (singleton == null) { 
  3.         singleton = new Singleton(); 
  4.     } 
  5.     return singleton; 

這個(gè)方法其實(shí)是存在問題的,試想一下,如果兩個(gè)線程同時(shí)判斷 singleton 為空,那么它們都會(huì)去實(shí)例化一個(gè)Singleton 對(duì)象,這就變成多例了。所以,我們要解決的是線程安全問題。

最容易想到的解決方法就是在方法上加鎖,或者是對(duì)類對(duì)象加鎖,程序就會(huì)變成下面這個(gè)樣子

  1. public static synchronized Singleton getInstance() { 
  2.     if (singleton == null) { 
  3.         singleton = new Singleton(); 
  4.     } 
  5.     return singleton; 
  6. // 或者 
  7. public static Singleton getInstance() { 
  8.     synchronized(Singleton.class) {    
  9.         if (singleton == null) { 
  10.             singleton = new Singleton(); 
  11.         } 
  12.     } 
  13.     return singleton; 

這樣就規(guī)避了兩個(gè)線程同時(shí)創(chuàng)建Singleton對(duì)象的風(fēng)險(xiǎn),但是引來另外一個(gè)問題:每次去獲取對(duì)象都需要先獲取鎖,并發(fā)性能非常地差,極端情況下,可能會(huì)出現(xiàn)卡頓現(xiàn)象。接下來要做的就是優(yōu)化性能:目標(biāo)是如果沒有實(shí)例化對(duì)象則加鎖創(chuàng)建,如果已經(jīng)實(shí)例化了,則不需要加鎖,直接獲取實(shí)例

所以直接在方法上加鎖的方式就被廢掉了,因?yàn)檫@種方式無論如何都需要先獲取鎖

  1. public static Singleton getInstance() { 
  2.     if (singleton == null) {  // 線程A和線程B同時(shí)看到singleton = null,如果不為null,則直接返回singleton 
  3.         synchronized(Singleton.class) { // 線程A或線程B獲得該鎖進(jìn)行初始化 
  4.             if (singleton == null) { // 其中一個(gè)線程進(jìn)入該分支,另外一個(gè)線程則不會(huì)進(jìn)入該分支 
  5.                 singleton = new Singleton(); 
  6.             } 
  7.         } 
  8.     } 
  9.     return singleton; 

上面的代碼已經(jīng)完美地解決了并發(fā)安全 + 性能低效問題:

  • 第 2 行代碼,如果 singleton 不為空,則直接返回對(duì)象,不需要獲取鎖;而如果多個(gè)線程發(fā)現(xiàn) singleton 為空,則進(jìn)入分支;
  • 第 3 行代碼,多個(gè)線程嘗試爭搶同一個(gè)鎖,只有一個(gè)線程爭搶成功,第一個(gè)獲取到鎖的線程會(huì)再次判斷singleton 是否為空,因?yàn)?singleton 有可能已經(jīng)被之前的線程實(shí)例化
  • 其它之后獲取到鎖的線程在執(zhí)行到第 4 行校驗(yàn)代碼,發(fā)現(xiàn) singleton 已經(jīng)不為空了,則不會(huì)再 new 一個(gè)對(duì)象,直接返回對(duì)象即可
  • 之后所有進(jìn)入該方法的線程都不會(huì)去獲取鎖,在第一次判斷 singleton 對(duì)象時(shí)已經(jīng)不為空了

因?yàn)樾枰獌纱闻锌?,且?duì)類對(duì)象加鎖,該懶漢式寫法也被稱為:Double Check(雙重校驗(yàn)) + Lock(加鎖)

完整的代碼如下所示:

  1. public class Singleton { 
  2.      
  3.     private static Singleton singleton; 
  4.      
  5.     private Singleton(){} 
  6.      
  7.     public static Singleton getInstance() { 
  8.         if (singleton == null) {  // 線程A和線程B同時(shí)看到singleton = null,如果不為null,則直接返回singleton 
  9.             synchronized(Singleton.class) { // 線程A或線程B獲得該鎖進(jìn)行初始化 
  10.                 if (singleton == null) { // 其中一個(gè)線程進(jìn)入該分支,另外一個(gè)線程則不會(huì)進(jìn)入該分支 
  11.                     singleton = new Singleton(); 
  12.                 } 
  13.             } 
  14.         } 
  15.         return singleton; 
  16.     } 
  17.      

上面這段代碼已經(jīng)近似完美了,但是還存在最后一個(gè)問題:指令重排

使用 volatile 防止指令重排

創(chuàng)建一個(gè)對(duì)象,在 JVM 中會(huì)經(jīng)過三步:

(1)為 singleton 分配內(nèi)存空間

(2)初始化 singleton 對(duì)象

(3)將 singleton 指向分配好的內(nèi)存空間

指令重排序是指:JVM 在保證最終結(jié)果正確的情況下,可以不按照程序編碼的順序執(zhí)行語句,盡可能提高程序的性能

在這三步中,第 2、3 步有可能會(huì)發(fā)生指令重排現(xiàn)象,創(chuàng)建對(duì)象的順序變?yōu)?1-3-2,會(huì)導(dǎo)致多個(gè)線程獲取對(duì)象時(shí),有可能線程 A 創(chuàng)建對(duì)象的過程中,執(zhí)行了 1、3 步驟,線程 B 判斷 singleton 已經(jīng)不為空,獲取到未初始化的singleton 對(duì)象,就會(huì)報(bào) NPE 異常。文字較為晦澀,可以看流程圖:

使用 volatile 關(guān)鍵字可以防止指令重排序,其原理較為復(fù)雜,這篇文章不打算展開,可以這樣理解:使用 volatile 關(guān)鍵字修飾的變量,可以保證其指令執(zhí)行的順序與程序指明的順序一致,不會(huì)發(fā)生順序變換,這樣在多線程環(huán)境下就不會(huì)發(fā)生 NPE 異常了。

volatile 還有第二個(gè)作用:使用 volatile 關(guān)鍵字修飾的變量,可以保證其內(nèi)存可見性,即每一時(shí)刻線程讀取到該變量的值都是內(nèi)存中最新的那個(gè)值,線程每次操作該變量都需要先讀取該變量。

最終的代碼如下所示:

  1. public class Singleton { 
  2.      
  3.     private static volatile Singleton singleton; 
  4.      
  5.     private Singleton(){} 
  6.      
  7.     public static Singleton getInstance() { 
  8.         if (singleton == null) {  // 線程A和線程B同時(shí)看到singleton = null,如果不為null,則直接返回singleton 
  9.             synchronized(Singleton.class) { // 線程A或線程B獲得該鎖進(jìn)行初始化 
  10.                 if (singleton == null) { // 其中一個(gè)線程進(jìn)入該分支,另外一個(gè)線程則不會(huì)進(jìn)入該分支 
  11.                     singleton = new Singleton(); 
  12.                 } 
  13.             } 
  14.         } 
  15.         return singleton; 
  16.     } 
  17.      

破壞懶漢式單例與餓漢式單例

無論是完美的懶漢式還是餓漢式,終究敵不過反射和序列化,它們倆都可以把單例對(duì)象破壞掉(產(chǎn)生多個(gè)對(duì)象)。

利用反射破壞單例模式

下面是一段使用反射破壞單例模式的例子

  1. public static void main(String[] args) { 
  2.     // 獲取類的顯式構(gòu)造器 
  3.     Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor(); 
  4.     // 可訪問私有構(gòu)造器 
  5.     construct.setAccessible(true);  
  6.     // 利用反射構(gòu)造新對(duì)象 
  7.     Singleton obj1 = construct.newInstance();  
  8.     // 通過正常方式獲取單例對(duì)象 
  9.     Singleton obj2 = Singleton.getInstance();  
  10.     System.out.println(obj1 == obj2); // false 

上述的代碼一針見血了:利用反射,強(qiáng)制訪問類的私有構(gòu)造器,去創(chuàng)建另一個(gè)對(duì)象

利用序列化與反序列化破壞單例模式

下面是一種使用序列化和反序列化破壞單例模式的例子

  1. public static void main(String[] args) { 
  2.     // 創(chuàng)建輸出流 
  3.     ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file")); 
  4.     // 將單例對(duì)象寫到文件中 
  5.     oos.writeObject(Singleton.getInstance()); 
  6.     // 從文件中讀取單例對(duì)象 
  7.     File file = new File("Singleton.file"); 
  8.     ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file)); 
  9.     Singleton newInstance = (Singleton) ois.readObject(); 
  10.     // 判斷是否是同一個(gè)對(duì)象 
  11.     System.out.println(newInstance == Singleton.getInstance()); // false 

兩個(gè)對(duì)象地址不相等的原因是:readObject() 方法讀入對(duì)象時(shí)它必定會(huì)返回一個(gè)新的對(duì)象實(shí)例,必然指向新的內(nèi)存地址。

讓面試官鼓掌的枚舉實(shí)現(xiàn)

我們已經(jīng)掌握了懶漢式與餓漢式的常見寫法了,通常情況下到這里已經(jīng)足夠了。但是,追求極致的我們,怎么能夠止步于此,在《Effective Java》書中,給出了終極解決方法,話不多說,學(xué)完下面,真的不虛面試官考你了。

在 JDK 1.5 后,使用 Java 語言實(shí)現(xiàn)單例模式的方式又多了一種:枚舉

枚舉實(shí)現(xiàn)單例模式完整代碼如下:

  1. public enum Singleton { 
  2.     INSTANCE; 
  3.      
  4.     public void doSomething() { 
  5.         System.out.println("這是枚舉類型的單例模式!"); 
  6.     } 

使用枚舉實(shí)現(xiàn)單例模式較其它兩種實(shí)現(xiàn)方式的優(yōu)勢有 3 點(diǎn),讓我們來細(xì)品。

優(yōu)勢 1 :一目了然的代碼

代碼對(duì)比餓漢式與懶漢式來說,更加地簡潔。最少只需要3行代碼,就可以完成一個(gè)單例模式:

  1. public enum Test { 
  2.     INSTANCE; 

我們從最直觀的地方入手,第一眼看到這3行代碼,就會(huì)感覺到少,沒錯(cuò),就是少,雖然這優(yōu)勢有些牽強(qiáng),但寫的代碼越少,越不容易出錯(cuò)。

優(yōu)勢 2:天然的線程安全與單一實(shí)例

它不需要做任何額外的操作,就可以保證對(duì)象單一性與線程安全性。

我寫了一段測試代碼放在下面,這一段代碼可以證明程序啟動(dòng)時(shí)僅會(huì)創(chuàng)建一個(gè) Singleton 對(duì)象,且是線程安全的。

我們可以簡單地理解枚舉創(chuàng)建實(shí)例的過程:在程序啟動(dòng)時(shí),會(huì)調(diào)用 Singleton 的空參構(gòu)造器,實(shí)例化好一個(gè)Singleton 對(duì)象賦給 INSTANCE,之后再也不會(huì)實(shí)例化

  1. public enum Singleton { 
  2.     INSTANCE; 
  3.     Singleton() { System.out.println("枚舉創(chuàng)建對(duì)象了"); } 
  4.     public static void main(String[] args) { /* test(); */ } 
  5.     public void test() { 
  6.         Singleton t1 = Singleton.INSTANCE; 
  7.         Singleton t2 = Singleton.INSTANCE; 
  8.         System.out.print("t1和t2的地址是否相同:" + t1 == t2); 
  9.     } 
  10. // 枚舉創(chuàng)建對(duì)象了 
  11. // t1和t2的地址是否相同:true 

除了優(yōu)勢1和優(yōu)勢2,還有最后一個(gè)優(yōu)勢是 保護(hù)單例模式,它使得枚舉在當(dāng)前的單例模式領(lǐng)域已經(jīng)是 無懈可擊 了

優(yōu)勢 3:枚舉保護(hù)單例模式不被破壞

使用枚舉可以防止調(diào)用者使用反射、序列化與反序列化機(jī)制強(qiáng)制生成多個(gè)單例對(duì)象,破壞單例模式。

防反射

枚舉類默認(rèn)繼承了 Enum 類,在利用反射調(diào)用 newInstance() 時(shí),會(huì)判斷該類是否是一個(gè)枚舉類,如果是,則拋出異常。

防止反序列化創(chuàng)建多個(gè)枚舉對(duì)象

在讀入 Singleton 對(duì)象時(shí),每個(gè)枚舉類型和枚舉名字都是唯一的,所以在序列化時(shí),僅僅只是對(duì)枚舉的類型和變量名輸出到文件中,在讀入文件反序列化成對(duì)象時(shí),使用 Enum 類的 valueOf(String name) 方法根據(jù)變量的名字查找對(duì)應(yīng)的枚舉對(duì)象。

所以,在序列化和反序列化的過程中,只是寫出和讀入了枚舉類型和名字,沒有任何關(guān)于對(duì)象的操作。

小結(jié):

(1)Enum 類內(nèi)部使用Enum 類型判定防止通過反射創(chuàng)建多個(gè)對(duì)象

(2)Enum 類通過寫出(讀入)對(duì)象類型和枚舉名字將對(duì)象序列化(反序列化),通過 valueOf() 方法匹配枚舉名找到內(nèi)存中的唯一的對(duì)象實(shí)例,防止通過反序列化構(gòu)造多個(gè)對(duì)象

(3)枚舉類不需要關(guān)注線程安全、破壞單例和性能問題,因?yàn)槠鋭?chuàng)建對(duì)象的時(shí)機(jī)與餓漢式單例有異曲同工之妙。

總結(jié)

(1)單例模式常見的寫法有兩種:懶漢式、餓漢式

(2)懶漢式:在需要用到對(duì)象時(shí)才實(shí)例化對(duì)象,正確的實(shí)現(xiàn)方式是:Double Check + Lock,解決了并發(fā)安全和性能低下問題

(3)餓漢式:在類加載時(shí)已經(jīng)創(chuàng)建好該單例對(duì)象,在獲取單例對(duì)象時(shí)直接返回對(duì)象即可,不會(huì)存在并發(fā)安全和性能問題。

(4)在開發(fā)中如果對(duì)內(nèi)存要求非常高,那么使用懶漢式寫法,可以在特定時(shí)候才創(chuàng)建該對(duì)象;

(5)如果對(duì)內(nèi)存要求不高使用餓漢式寫法,因?yàn)楹唵尾灰壮鲥e(cuò),且沒有任何并發(fā)安全和性能問題

(6)為了防止多線程環(huán)境下,因?yàn)橹噶钪嘏判驅(qū)е伦兞繄?bào)NPE,需要在單例對(duì)象上添加 volatile 關(guān)鍵字防止指令重排序

(7)最優(yōu)雅的實(shí)現(xiàn)方式是使用枚舉,其代碼精簡,沒有線程安全問題,且 Enum 類內(nèi)部防止反射和反序列化時(shí)破壞單例。

本文轉(zhuǎn)載自微信公眾號(hào)「 Java建設(shè)者」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 Java建設(shè)者公眾號(hào)。

 

責(zé)任編輯:武曉燕 來源: Java建設(shè)者
相關(guān)推薦

2021-03-01 12:51:00

監(jiān)控產(chǎn)品經(jīng)理指標(biāo)

2009-10-26 08:58:14

LinuxWindows 7

2024-08-13 17:56:52

單例裝飾器模式

2021-11-02 22:04:58

模式

2021-02-16 10:53:19

單例模式面試

2020-08-03 07:38:12

單例模式

2021-12-02 08:19:06

MVCC面試數(shù)據(jù)庫

2019-08-23 09:20:35

Spring 5編程Java

2020-08-13 10:15:34

MySQL數(shù)據(jù)庫面試

2019-07-15 16:35:43

MySQL索引阿里

2020-07-02 07:52:11

RedisHash映射

2021-01-08 09:14:59

分布式事務(wù)框架

2020-05-22 11:00:22

單例Java代碼

2020-02-25 16:56:02

面試官有話想說

2020-08-11 10:40:31

裝飾者模式Java組件

2022-11-15 17:45:46

數(shù)據(jù)庫MySQL

2021-05-30 12:27:54

策略模式算法

2024-08-05 01:26:54

2021-03-24 10:25:24

優(yōu)化VUE性能

2009-09-28 10:58:45

招聘
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)