大家好,我是小簡(jiǎn),這一篇文章,6種單例方法一網(wǎng)打盡,雖然單例模式很簡(jiǎn)單,但是也是設(shè)計(jì)模式入門(mén)基礎(chǔ),我也來(lái)詳細(xì)講講。
DEMO倉(cāng)庫(kù):https://github.com/JanYork/DesignPattern ,歡迎PR,共建。
(資料圖片)
單例模式(SingletonPattern
)是 Java
中最簡(jiǎn)單的設(shè)計(jì)模式之一。
單例模式一共存在 --> 懶漢式、餓漢式、懶漢+同步鎖、雙重校驗(yàn)鎖、靜態(tài)內(nèi)部類(lèi)、枚舉這六種方式。
這種類(lèi)型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。
這種模式涉及到一個(gè)單一的類(lèi),該類(lèi)負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。
這個(gè)類(lèi)提供了一種訪(fǎng)問(wèn)其唯一的對(duì)象的方式,可以直接訪(fǎng)問(wèn),不需要實(shí)例化該類(lèi)的對(duì)象。
Spring
的Bean
默認(rèn)就是單例的,全局唯一。單例的原理非常簡(jiǎn)單,我們讓他唯一的方法就是讓他不可用被new
,那我們只需要私有化類(lèi)的構(gòu)造即可:
private ClassName() {}
但是私有化后,我們不能new
又如何創(chuàng)建對(duì)象呢?
我們首先要明白,private
他是私有的,也就是不讓外部其他類(lèi)訪(fǎng)問(wèn),那我們自己還是可以訪(fǎng)問(wèn)的,所以在上文的要求中就說(shuō)到了:單例類(lèi)必須自己創(chuàng)建自己的唯一實(shí)例。
同時(shí)我們還需要拋出單例的獲取方法。
public class SlackerStyle {}
public class SlackerStyle { private static SlackerStyle instance;}
public class SlackerStyle { private static SlackerStyle instance; /** * 私有化構(gòu)造方法(防止外部new新的對(duì)象) */ private SlackerStyle() { }}
public class SlackerStyle { private static SlackerStyle instance; /** * 私有化構(gòu)造方法(防止外部new新的對(duì)象) */ private SlackerStyle() { } /** * 提供一個(gè)靜態(tài)的公有方法,當(dāng)使用到該方法時(shí),才去創(chuàng)建instance * 即懶漢式 * * @return instance(單例對(duì)象) */ public static SlackerStyle getInstance() { if (instance == null) { instance = new SlackerStyle(); } return instance; }}
當(dāng)我們調(diào)用靜態(tài)方法,它便會(huì)判斷上面的靜態(tài)屬性
instance
中有無(wú)自身對(duì)象,無(wú) --> 創(chuàng)建對(duì)象并賦值給instance
,有 --> 返回instance
。
優(yōu)點(diǎn):延遲加載,效率較高。缺點(diǎn):線(xiàn)程不安全,可能會(huì)造成多個(gè)實(shí)例。
解釋?zhuān)貉舆t加載 --> 懶漢式只有在需要時(shí)才會(huì)創(chuàng)建單例對(duì)象,可以節(jié)約資源并提高程序的啟動(dòng)速度。
在以上的類(lèi)中,對(duì)getInstance()
方法添加synchronized
鎖,即可彌補(bǔ)線(xiàn)程不安全缺陷。
/** * 注意,此段為補(bǔ)充,為了解決線(xiàn)程不安全的問(wèn)題,可以在方法上加上synchronized關(guān)鍵字,但是這樣會(huì)導(dǎo)致效率下降 * 提供一個(gè)靜態(tài)的公有方法,加入同步處理的代碼,解決線(xiàn)程安全問(wèn)題 * 此方法為線(xiàn)程安全的懶漢式,即懶漢+同步鎖,就不額外寫(xiě)一個(gè)類(lèi)了 * * @return instance(單例對(duì)象) */ public static synchronized SlackerStyle getInstance2() { if (instance == null) { instance = new SlackerStyle(); } return instance; }
雖然彌補(bǔ)了線(xiàn)程不安全的缺陷,但是也失去了一部分效率,所以需要根據(jù)業(yè)務(wù)環(huán)境去選擇適合的方法,魚(yú)和熊掌不可兼得。
還是如開(kāi)始一樣,創(chuàng)建好單例類(lèi),私有化構(gòu)造方法。
public class HungryManStyle { /** * 私有化構(gòu)造方法(防止外部new新的對(duì)象) */ private HungryManStyle() { }}
我們餓漢式是延遲加載的,即要用,然后第一次去調(diào)用時(shí)才會(huì)創(chuàng)建對(duì)象,而餓漢式恰恰相反,他在初始化類(lèi)的時(shí)候就去創(chuàng)建。
我們的static
關(guān)鍵詞修飾的方法或?qū)傩裕陬?lèi)加載之初遍開(kāi)辟內(nèi)存創(chuàng)建好了相關(guān)的內(nèi)容了。
包括每個(gè)類(lèi)的:
static{}
中也一樣的。
所以我們直接使用static
修飾。
public class HungryManStyle { /** * 靜態(tài)變量(單例對(duì)象),類(lèi)加載時(shí)就初始化對(duì)象(不存在線(xiàn)程安全問(wèn)題) */ private static final HungryManStyle INSTANCE = new HungryManStyle(); /** * 私有化構(gòu)造方法(防止外部new新的對(duì)象) */ private HungryManStyle() { } /** * 提供一個(gè)靜態(tài)的公有方法,直接返回INSTANCE * * @return instance(單例對(duì)象) */ public static HungryManStyle getInstance() { return INSTANCE; }}
而且我們?cè)陬?lèi)的靜態(tài)屬性創(chuàng)建時(shí)就new
了一個(gè)自身對(duì)象了。
餓漢式的優(yōu)點(diǎn)如下:
線(xiàn)程安全:由于在類(lèi)加載時(shí)就創(chuàng)建單例對(duì)象,因此不存在多線(xiàn)程環(huán)境下的同步問(wèn)題。沒(méi)有加鎖的性能問(wèn)題:餓漢式?jīng)]有使用同步鎖,因此不存在加鎖帶來(lái)的性能問(wèn)題。實(shí)現(xiàn)簡(jiǎn)單:餓漢式的實(shí)現(xiàn)比較簡(jiǎn)單,不需要考慮多線(xiàn)程環(huán)境下的同步問(wèn)題。餓漢式的缺點(diǎn)如下:
立即加載:由于在類(lèi)加載時(shí)就創(chuàng)建單例對(duì)象,因此可能會(huì)影響程序的啟動(dòng)速度。浪費(fèi)資源:如果單例對(duì)象很大,并且程序中很少使用,那么餓漢式可能會(huì)浪費(fèi)資源。綜上所述,餓漢式的優(yōu)點(diǎn)是線(xiàn)程安全、沒(méi)有加鎖的性能問(wèn)題和實(shí)現(xiàn)簡(jiǎn)單,缺點(diǎn)是可能影響程序的啟動(dòng)速度和浪費(fèi)資源。
在選擇單例模式的實(shí)現(xiàn)方式時(shí),需要根據(jù)實(shí)際情況綜合考慮各種因素,選擇最適合的方式。
老規(guī)矩。
public class DoubleLockStyle { /** * volatile關(guān)鍵字,使得instance變量在多個(gè)線(xiàn)程間可見(jiàn),禁止指令重排序優(yōu)化 * volatile是一個(gè)輕量級(jí)的同步機(jī)制,即輕量鎖 */ private static volatile DoubleLockStyle instance; /** * 私有化構(gòu)造方法(防止外部new新的對(duì)象) */ private DoubleLockStyle() { }}
不一樣的是,我在屬性上使用volatile
關(guān)鍵詞修飾了。
補(bǔ)充知識(shí)啦!
在這個(gè)代碼中,使用了 volatile 關(guān)鍵字來(lái)確保 instance 變量的可見(jiàn)性,避免出現(xiàn)空指針異常等問(wèn)題。
volatile
是一種修飾符,用于修飾變量。當(dāng)一個(gè)變量被聲明為volatile
時(shí),線(xiàn)程在訪(fǎng)問(wèn)該變量時(shí)會(huì)強(qiáng)制從主內(nèi)存中讀取變量的值,而不是從線(xiàn)程的本地緩存中讀取。使用volatile
關(guān)鍵字可以保證多線(xiàn)程之間的變量訪(fǎng)問(wèn)具有可見(jiàn)性和有序性。在對(duì)該變量進(jìn)行修改時(shí),線(xiàn)程也會(huì)將修改后的值強(qiáng)制刷回主內(nèi)存,而不是僅僅更新線(xiàn)程的本地緩存。補(bǔ)充:
volatile
的主要作用是保證共享變量的可見(jiàn)性和有序性。共享變量是指在多個(gè)線(xiàn)程之間共享的變量,例如單例模式中的 instance
變量。如果不使用volatile
關(guān)鍵字修飾 instance
變量,在多線(xiàn)程環(huán)境下可能會(huì)出現(xiàn)空指針異常等問(wèn)題。
這是因?yàn)楫?dāng)一個(gè)線(xiàn)程修改了 instance
變量的值時(shí),其他線(xiàn)程可能無(wú)法立即看到修改后的值,從而出現(xiàn)空指針異常等問(wèn)題。
使用 volatile
關(guān)鍵字可以解決這個(gè)問(wèn)題,因?yàn)樗梢员WC對(duì)共享變量的修改對(duì)其他線(xiàn)程是可見(jiàn)的。
除了可見(jiàn)性和有序性之外,volatile 還可以防止指令重排序。指令重排序是指 CPU 為了提高程序執(zhí)行的效率而對(duì)指令執(zhí)行的順序進(jìn)行調(diào)整的行為。在單例模式中,如果 instance 變量沒(méi)有被聲明為 volatile,那么在多線(xiàn)程環(huán)境下可能會(huì)出現(xiàn)單例對(duì)象被重復(fù)創(chuàng)建的問(wèn)題。這是因?yàn)樵诙嗑€(xiàn)程環(huán)境下,某些線(xiàn)程可能會(huì)在 instance 變量被初始化之前就調(diào)用
getInstance()
方法,從而導(dǎo)致多次創(chuàng)建單例對(duì)象。通過(guò)將 instance 變量聲明為 volatile,可以保證在創(chuàng)建單例對(duì)象之前,instance 變量已經(jīng)被正確地初始化了。
/** * 提供一個(gè)靜態(tài)的公有方法,加入雙重檢查代碼,解決線(xiàn)程安全問(wèn)題,同時(shí)解決懶加載問(wèn)題 * 即雙重檢查鎖模式 * * @return instance(單例對(duì)象) */public static DoubleLockStyle getInstance() { if (instance == null) { // 同步代碼塊,線(xiàn)程安全的創(chuàng)建實(shí)例 synchronized (DoubleLockStyle.class) { //之所以要再次判斷,是因?yàn)榭赡苡卸鄠€(gè)線(xiàn)程同時(shí)進(jìn)入了第一個(gè)if判斷 if (instance == null) { instance = new DoubleLockStyle(); } } } return instance;}
在獲取方法中,使用synchronized
來(lái)同步,使它線(xiàn)程安全。
雙重鎖模式是一種用于延遲初始化的優(yōu)化模式,在第一次調(diào)用時(shí)創(chuàng)建單例對(duì)象,并在之后的訪(fǎng)問(wèn)中直接返回該對(duì)象。它通過(guò)使用雙重檢查鎖定(double checked locking)來(lái)保證在多線(xiàn)程環(huán)境下只有一個(gè)線(xiàn)程可以創(chuàng)建單例對(duì)象,并且不會(huì)加鎖影響程序性能。
優(yōu)點(diǎn):
線(xiàn)程安全:使用雙重鎖模式可以保證在多線(xiàn)程環(huán)境下只有一個(gè)線(xiàn)程可以創(chuàng)建單例對(duì)象,并且不會(huì)加鎖影響程序性能。延遲初始化:在第一次調(diào)用時(shí)創(chuàng)建單例對(duì)象,可以避免不必要的資源浪費(fèi)和內(nèi)存占用。性能優(yōu)化:通過(guò)使用雙重檢查鎖定,可以避免不必要的鎖競(jìng)爭(zhēng),從而提高程序性能。缺點(diǎn):
實(shí)現(xiàn)復(fù)雜:雙重鎖模式的實(shí)現(xiàn)相對(duì)復(fù)雜,需要考慮線(xiàn)程安全和性能等因素,容易出現(xiàn)錯(cuò)誤??勺x性差:由于雙重鎖模式的實(shí)現(xiàn)比較復(fù)雜,代碼可讀性較差,不易于理解和維護(hù)。難以調(diào)試:由于雙重鎖模式涉及到多線(xiàn)程并發(fā)訪(fǎng)問(wèn),因此在調(diào)試過(guò)程中可能會(huì)出現(xiàn)一些難以定位和復(fù)現(xiàn)的問(wèn)題。在雙重鎖模式中,確實(shí)只有一個(gè) synchronized
關(guān)鍵字,但是這個(gè) synchronized
關(guān)鍵字是在代碼中被使用了兩次,因此被稱(chēng)為“雙重鎖”。
具體來(lái)說(shuō),雙重鎖模式通常會(huì)在 getInstance
方法中使用 synchronized
關(guān)鍵字來(lái)保證線(xiàn)程安全,但是這會(huì)影響程序的性能,因?yàn)槊看卧L(fǎng)問(wèn) getInstance
方法都需要獲取鎖。為了避免這個(gè)問(wèn)題,雙重鎖模式使用了一個(gè)優(yōu)化技巧,即只有在第一次調(diào)用 getInstance
方法時(shí)才會(huì)獲取鎖并創(chuàng)建單例對(duì)象,以后的調(diào)用都直接返回已經(jīng)創(chuàng)建好的單例對(duì)象,不需要再獲取鎖。
具體實(shí)現(xiàn)時(shí),雙重鎖模式會(huì)在第一次調(diào)用 getInstance
方法時(shí)進(jìn)行兩次檢查,分別使用外部的 if
語(yǔ)句和內(nèi)部的 synchronized
關(guān)鍵字。外部的 if
語(yǔ)句用于判斷單例對(duì)象是否已經(jīng)被創(chuàng)建,如果已經(jīng)被創(chuàng)建則直接返回單例對(duì)象,否則進(jìn)入內(nèi)部的 synchronized
關(guān)鍵字塊,再次檢查單例對(duì)象是否已經(jīng)被創(chuàng)建,如果沒(méi)有被創(chuàng)建則創(chuàng)建單例對(duì)象并返回,否則直接返回已經(jīng)創(chuàng)建好的單例對(duì)象。
這樣做的好處是,在多線(xiàn)程環(huán)境下,只有一個(gè)線(xiàn)程可以進(jìn)入內(nèi)部的 synchronized
關(guān)鍵字塊,從而保證了線(xiàn)程安全,同時(shí)避免了每次訪(fǎng)問(wèn) getInstance
方法都需要獲取鎖的性能問(wèn)題。
因?yàn)橐呀?jīng)熟悉了這個(gè)設(shè)計(jì)模式原理,我就直接放代碼了。
public class StaticInnerClassStyle { /** * 私有化構(gòu)造方法(防止外部new新的對(duì)象) */ private StaticInnerClassStyle() { } /** * 靜態(tài)內(nèi)部類(lèi) */ private static class SingletonInstance { // 靜態(tài)內(nèi)部類(lèi)中的靜態(tài)變量(單例對(duì)象) private static final StaticInnerClassStyle INSTANCE = new StaticInnerClassStyle(); } /** * 提供一個(gè)靜態(tài)的公有方法,直接返回SingletonInstance.INSTANCE * * @return instance(單例對(duì)象) */ public static StaticInnerClassStyle getInstance() { return SingletonInstance.INSTANCE; }}
優(yōu)點(diǎn):
線(xiàn)程安全:靜態(tài)內(nèi)部類(lèi)在第一次使用時(shí)才會(huì)被加載,因此在多線(xiàn)程環(huán)境下也可以保證只有一個(gè)線(xiàn)程創(chuàng)建單例對(duì)象,避免了線(xiàn)程安全問(wèn)題。延遲加載:靜態(tài)內(nèi)部類(lèi)模式可以實(shí)現(xiàn)延遲加載,即只有在第一次調(diào)用getInstance
方法時(shí)才會(huì)加載內(nèi)部類(lèi)并創(chuàng)建單例對(duì)象,避免了在程序啟動(dòng)時(shí)就創(chuàng)建單例對(duì)象的開(kāi)銷(xiāo)。缺點(diǎn):
需要額外的類(lèi):靜態(tài)內(nèi)部類(lèi)模式需要定義一個(gè)額外的類(lèi)來(lái)實(shí)現(xiàn)單例模式,如果項(xiàng)目中有大量的單例對(duì)象,則會(huì)增加代碼量。無(wú)法傳遞參數(shù):靜態(tài)內(nèi)部類(lèi)模式無(wú)法接受參數(shù),因此無(wú)法在創(chuàng)建單例對(duì)象時(shí)傳遞參數(shù),這可能會(huì)對(duì)某些場(chǎng)景造成限制。總的來(lái)說(shuō),靜態(tài)內(nèi)部類(lèi)模式是一種性能高、線(xiàn)程安全的單例模式實(shí)現(xiàn)方式,適用于大部分場(chǎng)景。
如果需要傳遞參數(shù)或者需要頻繁創(chuàng)建單例對(duì)象,則可能需要考慮其他的實(shí)現(xiàn)方式。
懶加載即延時(shí)加載 --> 使用時(shí)采取創(chuàng)建對(duì)象。
在靜態(tài)內(nèi)部類(lèi)模式中,單例對(duì)象是在靜態(tài)內(nèi)部類(lèi)中被創(chuàng)建的。靜態(tài)內(nèi)部類(lèi)只有在第一次被使用時(shí)才會(huì)被加載,因此單例對(duì)象也是在第一次使用時(shí)被創(chuàng)建的。這樣就實(shí)現(xiàn)了延遲加載的效果,即在需要時(shí)才創(chuàng)建單例對(duì)象,避免了在程序啟動(dòng)時(shí)就創(chuàng)建單例對(duì)象的開(kāi)銷(xiāo)。
此外,靜態(tài)內(nèi)部類(lèi)中的靜態(tài)變量和靜態(tài)方法是在類(lèi)加載時(shí)被初始化的,而靜態(tài)內(nèi)部類(lèi)本身是非常輕量級(jí)的,加載和初始化的時(shí)間和開(kāi)銷(xiāo)都非常小。因此,靜態(tài)內(nèi)部類(lèi)模式既能夠?qū)崿F(xiàn)懶加載,又不會(huì)帶來(lái)太大的性能損失。
總之,它在靜態(tài)初始化意料之外,我相信也在你意料之外。
/** * @author JanYork * @date 2023/3/1 17:54 * @description 設(shè)計(jì)模式之單例模式(枚舉單例) * 優(yōu)點(diǎn):避免序列化和反序列化攻擊破壞單例,避免反射攻擊破壞單例(枚舉類(lèi)型構(gòu)造函數(shù)是私有的),線(xiàn)程安全,延遲加載,效率較高。 * 缺點(diǎn):代碼復(fù)雜度較高。 */public enum EnumerateSingletons { /** * 枚舉單例 */ INSTANCE; public void whateverMethod() { // TODO:do something ,在這里實(shí)現(xiàn)單例對(duì)象的功能 }}
在上述代碼中,INSTANCE
是 EnumSingleton
類(lèi)型的一個(gè)枚舉常量,表示單例對(duì)象的一個(gè)實(shí)例。由于枚舉類(lèi)型的特性,INSTANCE
會(huì)被自動(dòng)初始化為單例對(duì)象的一個(gè)實(shí)例,并且保證在整個(gè)應(yīng)用程序的生命周期中只有一個(gè)實(shí)例。
使用枚舉單例的方式非常簡(jiǎn)單,只需要通過(guò) EnumSingleton.INSTANCE
的方式來(lái)獲取單例對(duì)象即可。例如:
EnumerateSingletons singleton = EnumerateSingletons.INSTANCE;singleton.doSomething();
使用枚舉單例的好處在于,它是線(xiàn)程安全、序列化安全、反射安全的,而且代碼簡(jiǎn)潔明了,不容易出錯(cuò)。另外,枚舉單例還可以通過(guò)枚舉類(lèi)型的特性來(lái)添加其他方法和屬性,非常靈活。
枚舉單例的缺點(diǎn)相對(duì)來(lái)說(shuō)比較少,但是也存在一些限制:
不支持懶加載:枚舉類(lèi)型的實(shí)例創(chuàng)建是在類(lèi)加載的時(shí)候完成的,因此無(wú)法實(shí)現(xiàn)懶加載的效果。無(wú)法繼承:枚舉類(lèi)型不能被繼承,因此無(wú)法通過(guò)繼承來(lái)擴(kuò)展單例類(lèi)的功能。有些情況下不太方便使用:例如需要傳遞參數(shù)來(lái)創(chuàng)建單例對(duì)象的場(chǎng)景,使用枚舉單例可能不太方便。總之,枚舉單例是一種非常優(yōu)秀的單例實(shí)現(xiàn)方式,它具有線(xiàn)程安全、序列化安全、反射安全等優(yōu)點(diǎn),適用于大多數(shù)單例場(chǎng)景,但也存在一些限制和局限性。需要根據(jù)具體的場(chǎng)景來(lái)選擇合適的單例實(shí)現(xiàn)方式。
設(shè)計(jì)模式本就是業(yè)務(wù)中優(yōu)化一些設(shè)計(jì)帶來(lái)的概念性設(shè)計(jì),我們需要結(jié)合業(yè)務(wù)分析:
餓漢式:適用于單例對(duì)象較小、創(chuàng)建成本低、不需要懶加載的場(chǎng)景。懶漢式:雙重鎖:適用于多線(xiàn)程環(huán)境,對(duì)性能要求較高的場(chǎng)景。靜態(tài)內(nèi)部類(lèi):適用于多線(xiàn)程環(huán)境,對(duì)性能要求較高的場(chǎng)景。枚舉:適用于單例對(duì)象創(chuàng)建成本較高,且需要考慮線(xiàn)程安全、序列化安全、反射安全等問(wèn)題的場(chǎng)景。如果你的單例對(duì)象創(chuàng)建成本低、不需要考慮線(xiàn)程安全、序列化安全、反射安全等問(wèn)題,建議使用餓漢式實(shí)現(xiàn)單例;如果需要考慮線(xiàn)程安全和性能問(wèn)題,可以選擇懶漢式的雙重鎖或靜態(tài)內(nèi)部類(lèi)實(shí)現(xiàn)方式;如果需要考慮單例對(duì)象創(chuàng)建成本較高,需要考慮線(xiàn)程安全、序列化安全、反射安全等問(wèn)題,建議選擇枚舉單例實(shí)現(xiàn)方式。當(dāng)然,在實(shí)際的開(kāi)發(fā)中,還需要考慮其他一些因素,如單例對(duì)象的生命周期、多線(xiàn)程訪(fǎng)問(wèn)情況、性能要求、并發(fā)訪(fǎng)問(wèn)壓力等等,才能綜合選擇最合適的單例實(shí)現(xiàn)方式。
來(lái)自某AI(敏感詞):
關(guān)鍵詞: