1.概述
單例模式是設(shè)計(jì)模式中最簡單的一種,對于很多人來說,單例模式也是其接觸的第一種設(shè)計(jì)模式,當(dāng)然,我也不例外。這種設(shè)計(jì)模式在學(xué)習(xí)、面試、工作的過程中廣泛傳播,相信不少人在面試時(shí)遇到過這樣的問題:“說說你最熟悉的集中設(shè)計(jì)模式”,第一個(gè)脫口而出的就是單例模式。
所謂的單例模式,就是在一定的作用范圍內(nèi)保證只有一個(gè)實(shí)例,這種模式,簡單,但是想要使用好它,還需要學(xué)習(xí)一下它延伸出來的其他知識點(diǎn),本篇博文就對單例模式做一下簡單的整理,主要會包含以下幾部分內(nèi)容:
- 單例模式的代碼如何編寫?
- 是否需要嚴(yán)格的禁止單例被破壞?
- 餓漢式和懶漢式應(yīng)該如何選擇?
- 單例模式存在什么問題?
- 線程內(nèi)單例和進(jìn)程間單例如何實(shí)現(xiàn)?
- 什么叫做“多例模式”?
2.單例模式實(shí)現(xiàn)代碼
單例模式的實(shí)現(xiàn)代碼很多,下面會例舉一些常見的方式。
在Java
中,單例模式的作用范圍一般情況下指的是當(dāng)前的Java進(jìn)程,也就是進(jìn)程內(nèi)的對象保證唯一(當(dāng)然還有線程內(nèi)、進(jìn)程之間的單例,下面會提到),所以我們需要保證實(shí)例只會被初始化一次,如何保證呢?
2.1.餓漢式單例
一個(gè)簡單的做法,就是私有化構(gòu)造方法,也就是不讓外部的客戶端對象來調(diào)用new
方法,創(chuàng)建新的實(shí)例,而是在項(xiàng)目啟動(dòng)時(shí),由單例類自行初始化,這就是餓漢式單例
/**
* 餓漢式單例
*/
public class HungrySingleton {
private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return HUNGRY_SINGLETON;
}
}
2.2.懶漢式單例
如果不想再項(xiàng)目啟動(dòng)時(shí)初始化,而是在使用的時(shí)候再初始化對象,可以將對象的創(chuàng)建放到getInstance
方法中,這種方式叫做懶漢式單例。這種方式在多線程的情況下會有線程安全問題,需要在創(chuàng)建對象時(shí)加鎖。
/**
* 懶漢式單例
*/
public class LazySingleton {
private static volatile LazySingleton lazySingleton;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
2.3.雙檢鎖單例
在方法加的是類鎖,一次只有一個(gè)線程可以獲取到單例對象,為了提高獲取對象的效率,取消在方法上的類鎖,轉(zhuǎn)而只給創(chuàng)建對象的那一行代碼加鎖。但是在并發(fā)的情況下,多個(gè)線程同時(shí)進(jìn)入getInstance
方法,都可以通過lazySingleton == null
的判斷,并在加鎖那一行排隊(duì),每個(gè)線程都會創(chuàng)建一個(gè)新的對象,所以,我們需要在鎖里面再判斷一次對象是否創(chuàng)建。
這種在加鎖的代碼前后都進(jìn)行一次相同判斷的做法,我們叫做雙重檢查鎖,簡稱:雙檢鎖。
/**
* 雙檢鎖單例
*/
public class LazySingleton {
private static volatile LazySingleton lazySingleton;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (lazySingleton == null) {
synchronized (LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
}
2.4.靜態(tài)內(nèi)部類單例
上面的懶漢式是為了保證懶加載的同時(shí),又不能有線程安全問題,我們采用了加鎖的方式,那么不加鎖行不行呢?
當(dāng)然是可以的,我們采用靜態(tài)內(nèi)部類在受訪問時(shí)才會初始化的特性,來實(shí)現(xiàn)懶加載。
/**
* 靜態(tài)內(nèi)部類單例
*/
public class StaticClassSingleton {
private StaticClassSingleton() {
}
public static StaticClassSingleton getInstance() {
return InnerStaticClassSingleton.STATIC_CLASS_SINGLETON;
}
private static class InnerStaticClassSingleton {
private static final StaticClassSingleton STATIC_CLASS_SINGLETON = new StaticClassSingleton();
}
}
2.5.枚舉單例
Java中的枚舉是天然的單例模式,這種方式實(shí)現(xiàn)最簡單,而且不會有線程安全問題,也能避免通過反射或者反序列化創(chuàng)建新的對象。
下面代碼中的INSTANCE
就是單例對象了。
/**
* 枚舉單例
*/
public enum EnumSingleton {
INSTANCE;
}
3.對單例的一些思考
3.1.是否需要嚴(yán)格的禁止單例被破壞?
在上面的代碼例子中,我們采用的方式是私有化構(gòu)造方法方法來避免外部對象new
出新的單例對象,但這種方式并不能完全避免創(chuàng)建出新的對象。其實(shí)上面的枚舉單例中已經(jīng)提到了,可以通過反射、反序列化等方式,創(chuàng)建出新的對象。
在考慮如何避免通過反射、反序列化創(chuàng)建對象,可以先思考一下,有沒有必要去避免?
還是回到最初那個(gè)點(diǎn),我們用了這么多方式來實(shí)現(xiàn)單例模式,最終的目的就是為了在進(jìn)程內(nèi)的作用范圍內(nèi)只有一個(gè)實(shí)例。而在代碼的開發(fā)過程中,我們做好規(guī)范和約束,以人為的方式來控制對象的創(chuàng)建數(shù)量,哪怕沒有私有化構(gòu)造方法方法也能保證單例。而我們私有化構(gòu)造方法,更多的是給開發(fā)者做出一個(gè)提示,這個(gè)類是個(gè)單例類,并且防止一定的誤操作。
可以想象一下,我們要完全將各類創(chuàng)建和初始化的邏輯都“封閉”掉,代碼會臃腫到什么地步,而這部分“封閉”的邏輯在業(yè)務(wù)開發(fā)中我們完全使用不到,所以更建議以一種“約定由于配置”的方式來處理單例的創(chuàng)建問題。
綜上,做到私有化構(gòu)造方法這一步就夠了,不需要過度開發(fā)。
3.2.懶漢式真的比餓漢式更佳嗎?
懶漢式主要是為了做懶加載,當(dāng)單例對象沒有使用的時(shí)候就不創(chuàng)建和初始化,特別是初始化是需要加載的資源比較多、比較耗時(shí)的時(shí)候,用懶加載可以加快項(xiàng)目啟動(dòng)的速度,同時(shí)又能減少系統(tǒng)的資源浪費(fèi)。
但是從另一個(gè)角度講,如果不是在啟動(dòng)的時(shí)候初始化,那就是在客戶端調(diào)用的時(shí)候初始化,想象一下一個(gè)高并發(fā)的互聯(lián)網(wǎng)項(xiàng)目,如果在客戶端調(diào)用的時(shí)候再做耗時(shí)的初始化動(dòng)作,就可能造成接口的請求時(shí)間過長,接口超時(shí)等,會影響一批用戶的使用體驗(yàn)。
另外,如果初始化的過程中存在一些異常情況,我們應(yīng)該讓問題在項(xiàng)目啟動(dòng)時(shí)就暴露出來,及時(shí)修復(fù),而不是在用戶使用的時(shí)候才暴露問題。
綜上,在業(yè)務(wù)開發(fā)中或許餓漢式單例是更好的選擇。
3.3.單例存在的問題
單例模式編寫和使用都很簡單,但是它也存在一些問題,例如:
- 面向?qū)ο笾С植缓?/strong>:單例模式在作用范圍內(nèi)只有一個(gè)實(shí)例,那就無法通過創(chuàng)建更多的實(shí)例來使用面向?qū)ο蟮某橄?、繼承、多態(tài)等特性,更像是一種面向過程的寫法。
- 違反開閉原則:無法拓展,每一次迭代都需要修改原有的代碼。
- 違反單一職責(zé):單例模式既創(chuàng)建對象、又管理對象,職責(zé)模糊,可能會導(dǎo)致代碼變得復(fù)雜。
綜上,單例模式存在一定的問題,如果存在拓展的需求就盡可能的避免使用單例模式。
但如果在不需要大量的拓展,又沒有業(yè)務(wù)間的復(fù)雜依賴關(guān)系,使用單例模式就比較簡潔方便也不失為一種選擇,例如各種無狀態(tài)的工具類。
4.其他作用范圍的單例模式
4.1.線程內(nèi)的單例
即單例對象在線程內(nèi)時(shí)唯一的,線程之間不是唯一的,我們開發(fā)中有一種很常見的情況:線程局部變量,一般是通過ThreadLocal
來做的。
實(shí)現(xiàn)原理也比較簡單,其實(shí)就是使用一個(gè)全局的Map
來保存對象,以線程對象thread
為key
,以需要保存的單例對象為value
,這樣就保證了一個(gè)線程只對應(yīng)一個(gè)對象。
在業(yè)務(wù)流程中的用戶登錄信息,往往就是保存在ThreadLocal
中的,另外PageHelper
這個(gè)著名的工具類也是通過ThreadLocal
來實(shí)現(xiàn)的。
如果想了解ThreadLocal
的使用方式,可以參考我的另一篇博客《【并發(fā)編程】(九)線程安全的代碼及ThreadLocal的使用》
如果想了解它詳細(xì)的實(shí)現(xiàn)原理,可以參考《【并發(fā)編程】(十)線程局部變量——ThreadLocal原理詳解》
4.2.進(jìn)程間的單例
進(jìn)程間的單例,更常用的一種說法分布式環(huán)境中的單例,這類需求我們使用的也比較多,其實(shí)現(xiàn)原理也比較簡單,就是將單例對象通過序列化的方式存儲在一個(gè)多個(gè)服務(wù)都會共同訪問的存儲區(qū)域中,例如一個(gè)共享的文件中、一些分布式的中間件中,例如redis
、zk
等等,而最常見的當(dāng)然就是分布式鎖。
我們只需要為單例對象創(chuàng)建出一個(gè)唯一標(biāo)識,在每個(gè)服務(wù)中判斷唯一標(biāo)識是否存在即可。
5.“多例模式”
多例模式是單例模式中的一種特例,即可以在一定數(shù)量范圍內(nèi)創(chuàng)建類的多個(gè)實(shí)例,還有一層理解就是不同類型的對象可以創(chuàng)建多個(gè),想通類型的對象只能創(chuàng)建一個(gè),后者的概念使用的更多。
以日志打印為例,我們引入Slf4J
后通過下面的方式獲得一個(gè)日志對象:
private Logger logger = LoggerFactory.getLogger(xxx.class);
這里獲取的logger
如果后面的class
對象相同,獲取的就是同一個(gè)對象,這種方式更像是工廠模式,在代碼中看到的也是工廠模式,如下圖:
我們進(jìn)入這個(gè)工廠模式的方法后,可以看到下面的代碼:
這里就非常明顯了,這就是一種單例模式的創(chuàng)建方式,通過一個(gè)Map
將單例對象管理起來,如果Map
中有就直接返回,如果沒有就創(chuàng)建一個(gè)并放入到Map
中,這里的對象都是logger
對象,只是使用日志的類不一樣,這就是多例模式的一種體現(xiàn)。
另外,在Spring
中如果配置的bean
是單例的,其創(chuàng)建方式也與這種方式類似。文章來源:http://www.zghlxwxcb.cn/news/detail-723380.html
6.總結(jié)
本來主要講述了以下幾個(gè)點(diǎn):文章來源地址http://www.zghlxwxcb.cn/news/detail-723380.html
- 單例模式的編寫方式
餓漢式、懶漢式、靜態(tài)內(nèi)部類、枚舉 - 是否需要嚴(yán)格的禁止單例被破壞:
沒有必要寫的太嚴(yán)格,可以通過規(guī)范的方式來約束 - 餓漢式和懶漢式應(yīng)該如何選擇:
讓耗時(shí)操作提前初始化,讓問題提早暴露,及時(shí)修改,而不是讓用戶去發(fā)現(xiàn) - 單例模式存在什么問題
沒有面向?qū)ο螅卣剐圆?/li> - 線程內(nèi)單例和進(jìn)程間單例如何實(shí)現(xiàn)
線程內(nèi)單例通過線程局部變量來實(shí)現(xiàn),進(jìn)程間的單例通過共享的存儲區(qū)域來實(shí)現(xiàn) - 什么叫做“多例模式”
在一定數(shù)量范圍內(nèi)可以創(chuàng)建多個(gè),或者不同的類可以有多個(gè)、相同的類只能有一個(gè)
到了這里,關(guān)于【設(shè)計(jì)模式】單例模式、“多例模式”的實(shí)現(xiàn)以及對單例的一些思考的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!