日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

我向面试官讲解了单例模式,他对我竖起了大拇指

發布時間:2025/3/16 编程问答 61 豆豆
生活随笔 收集整理的這篇文章主要介紹了 我向面试官讲解了单例模式,他对我竖起了大拇指 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

作者:小菠蘿

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

?

什么是單例模式

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

單例模式是指在內存中只會創建且僅創建一次對象的設計模式。在程序中多次使用同一個對象且作用相同時,為了防止頻繁地創建對象使得內存飆升,單例模式可以讓程序僅在內存中創建一個對象,讓所有需要調用的地方都共享這一單例對象。

?


單例模式的類型

單例模式有兩種類型:

  • 懶漢式:在真正需要使用對象時才去創建該單例類對象

  • 餓漢式:在類加載時已經創建好該單例對象,等待被程序使用

懶漢式創建單例對象

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

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

public?class?Singleton?{private?static?Singleton?singleton;private?Singleton(){}public?static?Singleton?getInstance()?{if?(singleton?==?null)?{singleton?=?new?Singleton();}return?singleton;}}

沒錯,這里我們已經寫出了一個很不錯的單例模式,不過它不是完美的,但是這并不影響我們使用這個“單例對象”。

以上就是懶漢式創建單例對象的方法,我會在后面解釋這段代碼在哪里可以優化,存在什么問題。

餓漢式創建單例對象

餓漢式在類加載時已經創建好該對象,在程序調用時直接返回該單例對象即可,即我們在編碼時就已經指明了要馬上創建這個對象,不需要等到被調用時再去創建。

關于類加載,涉及到JVM的內容,我們目前可以簡單認為在程序啟動時,這個單例對象就已經創建好了。

public?class?Singleton{private?static?final?Singleton?singleton?=?new?Singleton();private?Singleton(){}public?static?Singleton?getInstance()?{return?singleton;} }

注意上面的代碼在第3行已經實例化好了一個Singleton對象在內存中,不會有多個Singleton對象實例存在

類在加載時會在堆內存中創建一個Singleton對象,當類被卸載時,Singleton對象也隨之消亡了。

? ?


懶漢式如何保證只創建一個對象

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

public?static?Singleton?getInstance()?{if?(singleton?==?null)?{singleton?=?new?Singleton();}return?singleton; }

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

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

public?static?synchronized?Singleton?getInstance()?{if?(singleton?==?null)?{singleton?=?new?Singleton();}return?singleton; } //?或者 public?static?Singleton?getInstance()?{synchronized(Singleton.class)?{???if?(singleton?==?null)?{singleton?=?new?Singleton();}}return?singleton; }

這樣就規避了兩個線程同時創建Singleton對象的風險,但是引來另外一個問題:每次去獲取對象都需要先獲取鎖,并發性能非常地差,極端情況下,可能會出現卡頓現象。接下來要做的就是優化性能:目標是如果沒有實例化對象則加鎖創建,如果已經實例化了,則不需要加鎖,直接獲取實例

所以直接在方法上加鎖的方式就被廢掉了,因為這種方式無論如何都需要先獲取鎖

public?static?Singleton?getInstance()?{if?(singleton?==?null)?{??//?線程A和線程B同時看到singleton?=?null,如果不為null,則直接返回singletonsynchronized(Singleton.class)?{?//?線程A或線程B獲得該鎖進行初始化if?(singleton?==?null)?{?//?其中一個線程進入該分支,另外一個線程則不會進入該分支singleton?=?new?Singleton();}}}return?singleton; }

上面的代碼已經完美地解決了并發安全 + 性能低效問題:

  • 第 2 行代碼,如果 singleton 不為空,則直接返回對象,不需要獲取鎖;而如果多個線程發現 singleton 為空,則進入分支;

  • 第 3 行代碼,多個線程嘗試爭搶同一個鎖,只有一個線程爭搶成功,第一個獲取到鎖的線程會再次判斷singleton 是否為空,因為 singleton 有可能已經被之前的線程實例化

  • 其它之后獲取到鎖的線程在執行到第 4 行校驗代碼,發現 singleton 已經不為空了,則不會再 new 一個對象,直接返回對象即可

  • 之后所有進入該方法的線程都不會去獲取鎖,在第一次判斷 singleton 對象時已經不為空了

因為需要兩次判空,且對類對象加鎖,該懶漢式寫法也被稱為:Double Check(雙重校驗) + Lock(加鎖)

完整的代碼如下所示:

public?class?Singleton?{private?static?Singleton?singleton;private?Singleton(){}public?static?Singleton?getInstance()?{if?(singleton?==?null)?{??//?線程A和線程B同時看到singleton?=?null,如果不為null,則直接返回singletonsynchronized(Singleton.class)?{?//?線程A或線程B獲得該鎖進行初始化if?(singleton?==?null)?{?//?其中一個線程進入該分支,另外一個線程則不會進入該分支singleton?=?new?Singleton();}}}return?singleton;}}

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

?


使用 volatile 防止指令重排

創建一個對象,在 JVM 中會經過三步:

(1)為 singleton 分配內存空間

(2)初始化 singleton 對象

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

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

在這三步中,第 2、3 步有可能會發生指令重排現象,創建對象的順序變為 1-3-2,會導致多個線程獲取對象時,有可能線程 A 創建對象的過程中,執行了 1、3 步驟,線程 B 判斷 singleton 已經不為空,獲取到未初始化的singleton 對象,就會報 NPE 異常。文字較為晦澀,可以看流程圖:

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

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

最終的代碼如下所示:

public?class?Singleton?{private?static?volatile?Singleton?singleton;private?Singleton(){}public?static?Singleton?getInstance()?{if?(singleton?==?null)?{??//?線程A和線程B同時看到singleton?=?null,如果不為null,則直接返回singletonsynchronized(Singleton.class)?{?//?線程A或線程B獲得該鎖進行初始化if?(singleton?==?null)?{?//?其中一個線程進入該分支,另外一個線程則不會進入該分支singleton?=?new?Singleton();}}}return?singleton;}}

?


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

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

利用反射破壞單例模式

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

public?static?void?main(String[]?args)?{//?獲取類的顯式構造器Constructor<Singleton>?construct?=?Singleton.class.getDeclaredConstructor();//?可訪問私有構造器construct.setAccessible(true);?//?利用反射構造新對象Singleton?obj1?=?construct.newInstance();?//?通過正常方式獲取單例對象Singleton?obj2?=?Singleton.getInstance();?System.out.println(obj1?==?obj2);?//?false }

上述的代碼一針見血了:利用反射,強制訪問類的私有構造器,去創建另一個對象

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

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

public?static?void?main(String[]?args)?{//?創建輸出流ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream("Singleton.file"));//?將單例對象寫到文件中oos.writeObject(Singleton.getInstance());//?從文件中讀取單例對象File?file?=?new?File("Singleton.file");ObjectInputStream?ois?=??new?ObjectInputStream(new?FileInputStream(file));Singleton?newInstance?=?(Singleton)?ois.readObject();//?判斷是否是同一個對象System.out.println(newInstance?==?Singleton.getInstance());?//?false }

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



讓面試官鼓掌的枚舉實現

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

在 JDK 1.5 后,使用 Java 語言實現單例模式的方式又多了一種:枚舉

枚舉實現單例模式完整代碼如下:

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

使用枚舉實現單例模式較其它兩種實現方式的優勢有 3 點,讓我們來細品。

優勢 1 :一目了然的代碼

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

public?enum?Test?{INSTANCE; }

我們從最直觀的地方入手,第一眼看到這3行代碼,就會感覺到少,沒錯,就是少,雖然這優勢有些牽強,但寫的代碼越少,越不容易出錯。

優勢 2:天然的線程安全與單一實例

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

我寫了一段測試代碼放在下面,這一段代碼可以證明程序啟動時僅會創建一個 Singleton 對象,且是線程安全的。

我們可以簡單地理解枚舉創建實例的過程:在程序啟動時,會調用 Singleton 的空參構造器,實例化好一個Singleton 對象賦給 INSTANCE,之后再也不會實例化

public?enum?Singleton?{INSTANCE;Singleton()?{?System.out.println("枚舉創建對象了");?}public?static?void?main(String[]?args)?{?/*?test();?*/?}public?void?test()?{Singleton?t1?=?Singleton.INSTANCE;Singleton?t2?=?Singleton.INSTANCE;System.out.print("t1和t2的地址是否相同:"?+?t1?==?t2);} } //?枚舉創建對象了 // t1和t2的地址是否相同:true

除了優勢1和優勢2,還有最后一個優勢是 保護單例模式,它使得枚舉在當前的單例模式領域已經是 無懈可擊 了

優勢 3:枚舉保護單例模式不被破壞

使用枚舉可以防止調用者使用反射、序列化與反序列化機制強制生成多個單例對象,破壞單例模式。

防反射


枚舉類默認繼承了 Enum 類,在利用反射調用 newInstance() 時,會判斷該類是否是一個枚舉類,如果是,則拋出異常。

防止反序列化創建多個枚舉對象

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

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

小結:

(1)Enum 類內部使用Enum 類型判定防止通過反射創建多個對象

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

(3)枚舉類不需要關注線程安全、破壞單例和性能問題,因為其創建對象的時機與餓漢式單例有異曲同工之妙。


?

總結

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

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

(3)餓漢式:在類加載時已經創建好該單例對象,在獲取單例對象時直接返回對象即可,不會存在并發安全和性能問題。

(4)在開發中如果對內存要求非常高,那么使用懶漢式寫法,可以在特定時候才創建該對象;

(5)如果對內存要求不高使用餓漢式寫法,因為簡單不易出錯,且沒有任何并發安全和性能問題

(6)為了防止多線程環境下,因為指令重排序導致變量報NPE,需要在單例對象上添加 volatile 關鍵字防止指令重排序

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

有道無術,術可成;有術無道,止于術

歡迎大家關注Java之道公眾號

好文章,我在看??

新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!

總結

以上是生活随笔為你收集整理的我向面试官讲解了单例模式,他对我竖起了大拇指的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。