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)線程安全,可以分為普通的懶漢模式這種線程不安全的寫(xiě)法,和餓漢模式,雙重檢查鎖的懶漢模式,以及通過(guò)靜態(tài)內(nèi)部類或者枚舉類等實(shí)現(xiàn)的線程安全的寫(xiě)法。
一個(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)改寫(xiě)上面的懶漢模式,代碼如下:
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; 改寫(xiě)成private static volatile DCLSingleton singleton; 這樣,就真正保證了線程同步的懶漢寫(xiě)法的單例模式。
餓漢寫(xiě)法
餓漢寫(xiě)法有很多變形,但無(wú)論是哪一種變形,都能保證線程安全,因?yàn)轲I漢寫(xiě)法是在類加載的時(shí)候,就完成了對(duì)象的初始化,類加載保證了他們天生是線程安全的。下面給出常見(jiàn)的2中餓漢寫(xiě)法
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)單例模式。寫(xiě)法如下:
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()方法的情況下,使用線程安全的寫(xiě)法確實(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ì)象。我們可以重寫(xiě)該方法,讓他返回原來(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)容,所以只有自己重寫(xiě)單例類的clone方法,如果不這樣,也只有枚舉能解決,因?yàn)槊杜e沒(méi)有克隆方法。
- 多分布式環(huán)境,因?yàn)槲覀兩鲜龊芏喾N單例的寫(xiě)法,都是依賴于類加載器的特性,但是static的作用只負(fù)責(zé)到類加載器,所以當(dāng)工程中存在多個(gè)類加載器的時(shí)候,就會(huì)創(chuàng)建多個(gè)實(shí)例,這種通常就需要第三方庫(kù)來(lái)解決。
什么時(shí)候用單例模式,用哪一種寫(xiě)法的單例模式
單例模式有兩種比較適合的使用場(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ì)使用的單例模式,如果需要理解的加載資源,就是用餓漢寫(xiě)法,在Android應(yīng)用中,很多對(duì)象需要在啟動(dòng)的時(shí)候,立即就使用,比如啟動(dòng)時(shí),需要拉取相機(jī)配置的類管理縮略圖的cache類等等。如果不是立即需要,或者不是貫穿應(yīng)用始終的,就不需要使用餓漢寫(xiě)法,可以考慮懶漢寫(xiě)法用(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)文章
IDEA 自定義方法注解模板的實(shí)現(xiàn)方法
這篇文章主要介紹了IDEA 自定義方法注解模板的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
使用easyexcel導(dǎo)出的excel文件,使用poi讀取時(shí)異常處理方案
這篇文章主要介紹了使用easyexcel導(dǎo)出的excel文件,使用poi讀取時(shí)異常處理方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
springboot2中使用@JsonFormat注解不生效的解決
這篇文章主要介紹了springboot2中使用@JsonFormat注解不生效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
解決Spring在Thread中注入Bean無(wú)效的問(wèn)題
這篇文章主要介紹了解決Spring在Thread中注入Bean無(wú)效的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
分享Spring Boot 3.x微服務(wù)升級(jí)歷程
Spring Boot 3.0.0 GA版已經(jīng)發(fā)布,好多人也開(kāi)始嘗試升級(jí),有人測(cè)試升級(jí)后,啟動(dòng)速度確實(shí)快了不少,這篇文章主要介紹了Spring Boot 3.x微服務(wù)升級(jí)經(jīng)歷,需要的朋友可以參考下2022-12-12

