Java設(shè)計(jì)模式之單例模式詳解
單例模式是非常常見(jiàn)的設(shè)計(jì)模式,其含義也很簡(jiǎn)單,一個(gè)類給外部提供一個(gè)唯一的實(shí)例。下文所有的代碼均在github
源碼整個(gè)項(xiàng)目不僅僅有設(shè)計(jì)模式,還有其他JavaSE知識(shí)點(diǎn),歡迎Star,F(xiàn)ork
單例模式的UML圖
單例模式的關(guān)鍵點(diǎn)
通過(guò)上面的UML圖,我們可以看出單例模式的特點(diǎn)如下:
1、構(gòu)造器是私有的,不允許外部的類調(diào)用構(gòu)造器
2、提供一個(gè)供外部訪問(wèn)的方法,該方法返回單例類的實(shí)例
如何實(shí)現(xiàn)單例模式
上面已經(jīng)給出了單例模式的關(guān)鍵點(diǎn),我們的實(shí)現(xiàn)只需要滿足上面2點(diǎn)即可。但是正因?yàn)閱卫J降膶?shí)現(xiàn)方式比較寬松,所以不同的實(shí)現(xiàn)方式會(huì)有不同的問(wèn)題。我們可以對(duì)單例模式的實(shí)現(xiàn)做一下分類,看一看有哪些不同的實(shí)現(xiàn)方式。
1根據(jù)單例對(duì)象的創(chuàng)建時(shí)機(jī)不同,可以分為餓漢模式和懶漢模式。餓漢是指在類加載的時(shí)候,就創(chuàng)建了對(duì)象。但是創(chuàng)建對(duì)象有時(shí)比較消耗資源,會(huì)造成類加載很慢,但是優(yōu)點(diǎn)是獲取對(duì)象的速度很快,因?yàn)樵缫呀?jīng)創(chuàng)建好了嘛。懶漢就是相對(duì)餓漢而言,在需要返回單例對(duì)象的時(shí)候,在創(chuàng)建對(duì)象,類加載的時(shí)候,并不初始化,好處與缺點(diǎn)也不言而喻
2.根據(jù)是否實(shí)現(xiàn)線程安全,可以分為普通的懶漢模式這種線程不安全的寫法,和餓漢模式,雙重檢查鎖的懶漢模式,以及通過(guò)靜態(tài)內(nèi)部類或者枚舉類等實(shí)現(xiàn)的線程安全的寫法。
一個(gè)線程不安全的單例模式
public class SimpleSingleton { private static SimpleSingleton simpleSingleton; private SimpleSingleton(){ } public static SimpleSingleton getInstance(){ if (simpleSingleton == null) { simpleSingleton = new SimpleSingleton(); } return simpleSingleton; } }
首先,我們可以看出這是一個(gè)懶漢模式的實(shí)現(xiàn)。因?yàn)橹挥性趃etInstance的時(shí)候,才會(huì)真正創(chuàng)建單例的對(duì)象。但是為什么他是線程不安全的呢,是因?yàn)榭赡軙?huì)有2個(gè)線程同時(shí)進(jìn)入if (simpleSingleton == null)的判斷,就是同時(shí)創(chuàng)建了simpleSingleton對(duì)象。
DCL懶漢模式
上面的方法可以看出是存在線程不安全的問(wèn)題的,我們可以用同步關(guān)鍵字synchronized來(lái)實(shí)現(xiàn)線程安全。我們先逐步分析,先用synchronized來(lái)改寫上面的懶漢模式,代碼如下:
public class DCLSingleton { private static DCLSingleton singleton; private DCLSingleton(){ } public synchronized static DClSingleton getSingleton(){ if (singleton == null) { singleton = new DCLSingleton(); } return singleton; } }
這樣,就有效的保證了不會(huì)有兩個(gè)線程同時(shí)執(zhí)行該方法,但這個(gè)效率也太低了吧。因?yàn)樵趧?chuàng)建實(shí)例之后,每次得到實(shí)例對(duì)象,還是需要進(jìn)行同步,synchronized的同步保證代價(jià)是比較大的,因此可以在此基礎(chǔ)上進(jìn)行改造。在已經(jīng)創(chuàng)建好之后,就不需要同步了,我們可以改成如下的形式:
public static DCLSingleton getSingleton(){ if (singleton == null) { synchronized (DCLSingleton.class) { if (singleton == null) { singleton = new DCLSingleton(); } } } return singleton; }
其他代碼不變,只看這個(gè)方法。該方法的兩重if (singleton == null)可以有效地保證線程安全。比如,當(dāng)兩個(gè)線程同時(shí)進(jìn)入該方法的時(shí)候,第一個(gè)if,兩者都是進(jìn)入,下面的代碼,但是碰到同步代碼塊,只能有一個(gè)先進(jìn)入,進(jìn)入的時(shí)候,繼續(xù)判斷,再次判斷為空,才會(huì)真正創(chuàng)建對(duì)象。如果不進(jìn)行,第二個(gè)判斷,那些對(duì)于第一個(gè)進(jìn)入的線程而言,確實(shí)創(chuàng)建了對(duì)象,但是第二個(gè)線程,他緊接著也會(huì)執(zhí)行創(chuàng)建對(duì)象的操作,因?yàn)椴恢赖谝粋€(gè)線程已經(jīng)創(chuàng)建成功。因此,需要兩次判空。
但是真的就如此簡(jiǎn)單的保證了線程安全嗎?我們仔細(xì)分析一下這個(gè)過(guò)程,singleton = new DCLSingleton();這個(gè)代碼實(shí)際上是3個(gè)操作。
1.給DCLSingleton實(shí)例分配內(nèi)存
2.調(diào)用DCLSingleton()的構(gòu)造函數(shù),初始化成員字段
3.將singleton對(duì)象指向分配的內(nèi)存空間。
在JDK1.5以前,上面的3個(gè)執(zhí)行順序是不固定的,有可能是1-2-3,或者1-3-2。如果是1-3-2,則在第一個(gè)線程執(zhí)行完第三步以后,第二個(gè)線程立即執(zhí)行,但還沒(méi)有真正的進(jìn)行初始化,所以就會(huì)使用的時(shí)候出錯(cuò)。在JDK1.5以后,我們可以用volatile關(guān)鍵字來(lái)保證該1-2-3的順序執(zhí)行。所以,除了getSingleton()方法要改成上面的樣子以外,還需要對(duì)private static DCLSingleton singleton; 改寫成private static volatile DCLSingleton singleton; 這樣,就真正保證了線程同步的懶漢寫法的單例模式。
餓漢寫法
餓漢寫法有很多變形,但無(wú)論是哪一種變形,都能保證線程安全,因?yàn)轲I漢寫法是在類加載的時(shí)候,就完成了對(duì)象的初始化,類加載保證了他們天生是線程安全的。下面給出常見(jiàn)的2中餓漢寫法
public class HungrySingleton { private static final HungrySingleton singleton = new HungrySingleton(); private HungrySingleton(){ } public static HungrySingleton getSingleton(){ return singleton; } } public class HungrySingleton { private static final HungrySingleton singleton = new HungrySingleton(); private HungrySingleton(){ } // public static HungrySingleton getSingleton(){ // return singleton; // } }
這兩種對(duì)初始化單例的對(duì)象上面,都是一致的, 通過(guò)final來(lái)保證對(duì)象的唯一。不同的是,調(diào)用單例對(duì)象的方式,第一種是通過(guò)getSingleton(),第二種是通過(guò)類.類變量的形式。
靜態(tài)內(nèi)部類實(shí)現(xiàn)單例模式
雙重檢查鎖(DCL)實(shí)現(xiàn)單例模式,雖然解決了線程不安全的問(wèn)題,以及保證了資源的懶加載,在需要的時(shí)候,才會(huì)進(jìn)行實(shí)例化的操作。但是在某些情況下(比如JDK低于1.5)會(huì)出現(xiàn)DCL失效,所以有一種很簡(jiǎn)潔且依舊是懶加載的方法實(shí)現(xiàn)單例模式。寫法如下:
public class StaticSingleton { private StaticSingleton(){ } public static final StaticSingleton getInstance(){ return Holder.singleton; } private static class Holder{ private static final StaticSingleton singleton = new StaticSingleton(); } }
通過(guò)靜態(tài)內(nèi)部類的形式,實(shí)現(xiàn)單例類的初始化,其特性同樣是通過(guò)ClassLoader來(lái)保證其單例對(duì)象的唯一,但是這是懶加載的,因?yàn)橹挥性贖older類被調(diào)用的時(shí)候,即getInstance調(diào)用的時(shí)候,才會(huì)加載Holder類從而實(shí)現(xiàn)創(chuàng)建對(duì)象。
枚舉類實(shí)現(xiàn)單例模式
直接看代碼:
public enum EnumSingleton { SINGLETON; public void doSometings(){ } }
使用的時(shí)候,直接通過(guò)EnumSingleton.SINGLETON.doSomethings()。枚舉類天生特性是保證不會(huì)有兩個(gè)實(shí)例,并且只有在第一次訪問(wèn)的時(shí)候才會(huì)被實(shí)例化,是懶加載的情況。
真的不會(huì)再次創(chuàng)建新的對(duì)象嗎?
在常規(guī)調(diào)用單例類的getInstance()方法的情況下,使用線程安全的寫法確實(shí)不會(huì)創(chuàng)建新的對(duì)象,但是Java提供了很多奇特的技巧和使用,下面這些使用會(huì)破壞掉常規(guī)的單例。
- 反序列化
- 反射
- 克隆
- 分布式環(huán)境下,多個(gè)類加載器
在除了枚舉實(shí)現(xiàn)單例模式的方法以外,其余所有方法碰到上述四種情況,都會(huì)重新創(chuàng)建對(duì)象。原因如下:
- 反序列化會(huì)調(diào)用一個(gè)特殊的readResolve()方法來(lái)創(chuàng)建新的對(duì)象。我們可以重寫該方法,讓他返回原來(lái)的instance,而不是重新創(chuàng)建一個(gè)。
- 反射會(huì)得到私有的構(gòu)造函數(shù),只能在構(gòu)造函數(shù)中加一個(gè)判斷,如果對(duì)象不為null,則扔出一個(gè)運(yùn)行時(shí)異常,如果不這樣,只有枚舉能解決,因?yàn)槊杜e自帶的特性。
- 克隆,因?yàn)橹苯涌截惖膬?nèi)存空間的內(nèi)容,所以只有自己重寫單例類的clone方法,如果不這樣,也只有枚舉能解決,因?yàn)槊杜e沒(méi)有克隆方法。
- 多分布式環(huán)境,因?yàn)槲覀兩鲜龊芏喾N單例的寫法,都是依賴于類加載器的特性,但是static的作用只負(fù)責(zé)到類加載器,所以當(dāng)工程中存在多個(gè)類加載器的時(shí)候,就會(huì)創(chuàng)建多個(gè)實(shí)例,這種通常就需要第三方庫(kù)來(lái)解決。
什么時(shí)候用單例模式,用哪一種寫法的單例模式
單例模式有兩種比較適合的使用場(chǎng)景。
第一種是創(chuàng)建某個(gè)對(duì)象,需要的代價(jià)比較大,為了避免頻繁的創(chuàng)建和銷毀對(duì)象從而引起的對(duì)資源的浪費(fèi),會(huì)考慮使用單例模式。
第二種是這個(gè)對(duì)象必須只有一個(gè),有多個(gè)會(huì)造成不可預(yù)估的錯(cuò)誤,或者程序的混亂,比如只會(huì)有一個(gè)序號(hào)生成器,一個(gè)緩存等等。
針對(duì)使用的單例模式,如果需要理解的加載資源,就是用餓漢寫法,在Android應(yīng)用中,很多對(duì)象需要在啟動(dòng)的時(shí)候,立即就使用,比如啟動(dòng)時(shí),需要拉取相機(jī)配置的類管理縮略圖的cache類等等。如果不是立即需要,或者不是貫穿應(yīng)用始終的,就不需要使用餓漢寫法,可以考慮懶漢寫法用(DCL或者靜態(tài)內(nèi)部類實(shí)現(xiàn))這兩種在一般情況下都不會(huì)出現(xiàn)問(wèn)題。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- java設(shè)計(jì)模式之單例模式學(xué)習(xí)
- 淺析Java設(shè)計(jì)模式編程中的單例模式和簡(jiǎn)單工廠模式
- Java設(shè)計(jì)模式之單例模式實(shí)例詳解【懶漢式與餓漢式】
- java設(shè)計(jì)模式之單例模式的詳解及優(yōu)點(diǎn)
- 深入解析Java的設(shè)計(jì)模式編程中單例模式的使用
- Java設(shè)計(jì)模式之單例模式實(shí)例分析
- 簡(jiǎn)單講解在Java編程中實(shí)現(xiàn)設(shè)計(jì)模式中的單例模式結(jié)構(gòu)
- java 設(shè)計(jì)模式之單例模式
- Java設(shè)計(jì)模式系列之深入淺出單例模式
相關(guān)文章
Java設(shè)計(jì)模式七大原則之接口隔離原則詳解
接口隔離原則(Interface Segregation Principle),又稱為ISP原則,就是在一個(gè)類中不要定義過(guò)多的方法,接口應(yīng)該盡量簡(jiǎn)單細(xì)化。本文將為大家具體介紹一下Java設(shè)計(jì)模式七大原則之一的接口隔離原則,需要的可以參考一下2022-02-02java實(shí)現(xiàn)Object和Map之間的轉(zhuǎn)換3種方式
本篇文章主要介紹了java實(shí)現(xiàn)Object和Map之間的轉(zhuǎn)換3種方式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06Springboot配置管理Externalized?Configuration深入探究
這篇文章主要介紹了Springboot配置管Externalized?Configuration深入探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Java實(shí)現(xiàn)滑動(dòng)驗(yàn)證碼(前端部分)
這篇文章主要為大家介紹了如何用Java語(yǔ)言實(shí)現(xiàn)滑動(dòng)驗(yàn)證碼的生成(前端部分),文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以跟隨小編學(xué)習(xí)一下2022-10-10Java中的傳值與傳引用實(shí)現(xiàn)過(guò)程解析
這篇文章主要介紹了java中的傳值與傳引用實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10Java集合Map常見(jiàn)問(wèn)題_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)整理了Java集合Map常見(jiàn)問(wèn)題,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05Java實(shí)現(xiàn)排球比賽計(jì)分系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)排球比賽計(jì)分系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06