前言
說(shuō)到設(shè)計(jì)模式,面試排在第一位的十有八九是單例模式,這一定是大部分人從入門到面試工作都避不開(kāi)的基礎(chǔ)知識(shí)。
但單例模式不僅有懶漢模式和餓漢模式兩種寫(xiě)法,往往我們掌握的都是最基礎(chǔ)的寫(xiě)法,如果你有閱讀過(guò)類似spring這樣的知名框架源碼,一定會(huì)發(fā)現(xiàn)他們的單例模式寫(xiě)法和你所掌握的完全不同。
本篇就給大家?guī)?lái)單例模式從基礎(chǔ)->最優(yōu)->額外推薦的寫(xiě)法,幫助你面試瘋狂加分。
懶漢餓漢
1、餓漢模式
餓漢模式簡(jiǎn)單理解就是提前創(chuàng)建好了對(duì)象
優(yōu)點(diǎn):寫(xiě)法簡(jiǎn)單,沒(méi)有線程同步的問(wèn)題
缺點(diǎn):因?yàn)橐崆皠?chuàng)建好對(duì)象,不管使用與否都一直占著內(nèi)存
推薦:對(duì)象較小且簡(jiǎn)單則使用餓漢模式
public final class Singleton {
// 創(chuàng)建好實(shí)例
private static Singleton instance = new Singleton();
// 構(gòu)造函數(shù)
private Singleton() {}
// 獲取實(shí)例
public static Singleton getInstance() {
return instance;
}
}
2、懶漢模式
懶漢模式簡(jiǎn)單理解就是在需要時(shí)才創(chuàng)建對(duì)象
優(yōu)點(diǎn):懶加載方式性能更高
缺點(diǎn):要考慮多線程的同步問(wèn)題
推薦:只要不符合上面餓漢的推薦使用條件則都使用懶漢模式
public final class Singleton {
private static Singleton instance = null;
// 構(gòu)造函數(shù)
private Singleton() {}
// 獲取實(shí)例
public static Singleton getInstance() {
// 為null時(shí)才實(shí)例化對(duì)象
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
同步鎖
上面介紹了懶漢的缺點(diǎn)是多線程同步的問(wèn)題,那么馬上就能想到使用同步鎖來(lái)解決這個(gè)問(wèn)題。
這里使用synchronized關(guān)鍵字且通過(guò)代碼塊來(lái)降低鎖粒度,最大程度保證了性能開(kāi)銷,其實(shí)從java8以后,synchronized的性能已經(jīng)有了較大提升。
public final class Singleton {
private static Singleton instance = null;
// 構(gòu)造函數(shù)
private Singleton() {}
// 獲取實(shí)例
public static Singleton getInstance() {
// 獲取對(duì)象時(shí)加上同步鎖
if (null == instance) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
雙重檢查鎖
上面雖然使用了同步鎖代碼塊,勉強(qiáng)解決了線程同步的問(wèn)題且性能開(kāi)銷做了最大程度的優(yōu)化,可實(shí)際上在多線程環(huán)境下仍然存在線程安全問(wèn)題。
當(dāng)依然有多個(gè)線程進(jìn)入到if判斷里面時(shí),這個(gè)線程安全問(wèn)題還是存在,雖然這種情況并非一定出現(xiàn),可極端情況下出現(xiàn)的幾率非常大。
這個(gè)時(shí)候就需要使用面試中關(guān)于設(shè)計(jì)模式很喜歡問(wèn)到的DCL即雙重檢查鎖模式,聽(tīng)起來(lái)很高大上,其實(shí)就是多加了一層判斷。
說(shuō)白了,就是在進(jìn)入同步鎖之前和之后分別進(jìn)行了檢查,極大降低了線程安全問(wèn)題。
public final class Singleton {
private static Singleton instance = null;
// 構(gòu)造函數(shù)
private Singleton() {}
// 獲取實(shí)例
public static Singleton getInstance() {
// 第一次判斷,當(dāng)instance為null時(shí)則實(shí)例化對(duì)象
if(null == instance) {
synchronized (Singleton.class) {
// 第二次判斷,放在同步鎖中,當(dāng)instance為null時(shí)則實(shí)例化對(duì)象
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
最優(yōu)雙重檢查鎖
雙重檢查鎖方式是單例懶漢模式在多線程下處理安全問(wèn)題的最佳方案之一,但上面依然不是最優(yōu)寫(xiě)法。
這里就要引出一個(gè)「指令重排」的概念,這個(gè)概念是java內(nèi)存模型中的,我這里用最簡(jiǎn)潔的方式幫你理解。
Java中new一個(gè)對(duì)象在內(nèi)存中執(zhí)行指令的正常順序是:分配 -> 創(chuàng)建 -> 引用,而多線程環(huán)境下,JVM出于對(duì)語(yǔ)句的優(yōu)化,有可能重排順序:分配 -> 引用 -> 創(chuàng)建。
如果出現(xiàn)這種情況,那么上面的雙重檢查鎖方式依然無(wú)法解決線程安全問(wèn)題。
解決方式很簡(jiǎn)單,加個(gè)volatile關(guān)鍵字即可。
volatile關(guān)鍵字作用:保證可見(jiàn)性和有序性。
public final class Singleton {
// 加上volatile關(guān)鍵字
private volatile static Singleton instance = null;
// 構(gòu)造函數(shù)
private Singleton() {}
// 獲取實(shí)例
public static Singleton getInstance() {
// 第一次判斷,當(dāng)instance為null時(shí)則實(shí)例化對(duì)象
if(null == instance) {
synchronized (Singleton.class) {
// 第二次判斷,放在同步鎖中,當(dāng)instance為null時(shí)則實(shí)例化對(duì)象
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
枚舉模式
《Effective Java》是Java業(yè)界非常受歡迎的一本書(shū),對(duì)于想要在Java領(lǐng)域深耕的程序員來(lái)講,這本書(shū)沒(méi)有不看的理由,相信很多Java程序員不管看過(guò)還是沒(méi)看過(guò),都有聽(tīng)過(guò)這本書(shū)。
而這本書(shū)的作者,所推薦的一種單例設(shè)計(jì)模式寫(xiě)法,就是枚舉方式。
原理十分簡(jiǎn)單,在Java中枚舉類的域在編譯后會(huì)被聲明為static屬性,而JVM會(huì)保證static修飾的成員變量只被實(shí)例化一次。
public class Singleton {
// 構(gòu)造函數(shù)
private Singleton() {
}
// 從枚舉中獲取實(shí)例
public static Singleton getInstance() {
return SingletonEnum.SINGLETON.getInstance();
}
// 定義枚舉
private enum SingletonEnum {
SINGLETON;
private Singleton instance;
// JVM保證這個(gè)方法只調(diào)用一次
SingletonEnum() {
instance = new Singleton();
}
public Singleton getInstance() {
return instance;
}
}
}
總結(jié)
最后這里稍微提一下,以免部分人對(duì)于設(shè)計(jì)模式感到些許負(fù)擔(dān)。
單例模式其實(shí)很簡(jiǎn)單,餓漢模式和懶漢模式在許多開(kāi)源框架中應(yīng)用都比較廣泛,甚至餓漢模式用的更多,比如Java的Runtime類中就這么干的,簡(jiǎn)單粗暴,有興趣的可以自己看下源碼。
難道這些框架的作者就意識(shí)不到本篇中講述的問(wèn)題嗎,并非如此,用哪種方式編寫(xiě)單例模式往往視情況而定,一些理論上會(huì)發(fā)生的問(wèn)題往往實(shí)際中可以忽略不計(jì),此時(shí)更傾向于使用最簡(jiǎn)單直接的寫(xiě)法。
真正難的其實(shí)還是面試,不少關(guān)于單例模式的問(wèn)題中喜歡問(wèn)到它的幾種寫(xiě)法、存在的問(wèn)題以及最佳方案,說(shuō)白了還是面試造核彈,進(jìn)廠擰螺絲,目的是想知道你對(duì)設(shè)計(jì)模式的了解程度,從而評(píng)判你研究該學(xué)科的態(tài)度及造詣。
因此,大家看完本篇可以手動(dòng)嘗試著寫(xiě)一寫(xiě),了解一些也就夠了,沒(méi)必要過(guò)分深究,因?yàn)镴ava領(lǐng)域需要花費(fèi)精力的地方確實(shí)太多了。
心得
最后說(shuō)下我的一點(diǎn)心得,所謂設(shè)計(jì)模式固然能給Java代碼本身帶來(lái)更多優(yōu)雅,但是寫(xiě)了很多年Java代碼,我大體還是覺(jué)得Java本身的裝飾實(shí)在太多,優(yōu)雅換來(lái)的往往是代碼本身的負(fù)擔(dān)。
我參與過(guò)的研發(fā)團(tuán)隊(duì)中,幾乎都能見(jiàn)到許多工程師編寫(xiě)的比較優(yōu)雅的代碼,一些設(shè)計(jì)模式也寫(xiě)的很好,可帶來(lái)的問(wèn)題也很明顯,就是可讀性越來(lái)越差,要求每個(gè)團(tuán)員都對(duì)Java有較高的造詣,甚至在某些時(shí)候給人力資源帶來(lái)壓力,這從實(shí)際角度考慮是不妥的。
我更多的建議是,應(yīng)對(duì)面試或?qū)W習(xí)好好領(lǐng)悟設(shè)計(jì)模式,百利而無(wú)一害,但實(shí)際工作中盡量少用復(fù)雜的設(shè)計(jì)模式,以簡(jiǎn)潔直接的代碼為主,有利于整個(gè)團(tuán)隊(duì)后期維護(hù),甚至加快人員變更后新成員對(duì)項(xiàng)目的適應(yīng)度,因?yàn)楣ぷ髡f(shuō)白了還是以績(jī)效為主,怎么簡(jiǎn)單高效怎么來(lái)就行,你自己的個(gè)人項(xiàng)目你想怎么玩隨便你。
本人原創(chuàng)文章純手打,覺(jué)得有一滴滴幫助的話就請(qǐng)點(diǎn)個(gè)贊和收藏吧~
本人長(zhǎng)期分享工作中的感悟、經(jīng)驗(yàn)及實(shí)用案例,喜歡的話也可以進(jìn)入個(gè)人主頁(yè)關(guān)注一下哦~