天跟大家講一個(gè)老生常談的話題,單例模式是最常用到的設(shè)計(jì)模式之一腹暖,熟悉設(shè)計(jì)模式的朋友對(duì)單例模式都不會(huì)陌生。網(wǎng)上的文章也很多翰萨,但是參差不齊微服,良莠不齊,要么說(shuō)的不到點(diǎn)子上缨历,要么寫的不完整,我試圖寫一篇史上最全單例模式糙麦,讓你看一篇文章就夠了辛孵。
單例模式定義及應(yīng)用場(chǎng)景
單例模式是指確保一個(gè)類在任何情況下都絕對(duì)只有一個(gè)實(shí)例,并提供一個(gè)全局訪問(wèn)點(diǎn)赡磅。單例模式是創(chuàng)建型模式魄缚。許多時(shí)候整個(gè)系統(tǒng)只需要擁有一個(gè)全局對(duì)象,這樣有利于我們協(xié)調(diào)系統(tǒng)整體的行為焚廊。
比如在某個(gè)服務(wù)器程序中冶匹,該服務(wù)器的配置信息存放在一個(gè)文件中,這些配置數(shù)據(jù)由一個(gè)單例對(duì)象統(tǒng)一讀取咆瘟,然后服務(wù)進(jìn)程中的其他對(duì)象再通過(guò)這個(gè)單例對(duì)象獲取這些配置信息嚼隘。這種方式簡(jiǎn)化了在復(fù)雜環(huán)境下的配置管理。
我們寫單例的思路是袒餐,隱藏其所有構(gòu)造方法飞蛹,提供一個(gè)全局訪問(wèn)點(diǎn)谤狡。
1、餓漢式
這個(gè)很簡(jiǎn)單卧檐,小伙們都寫過(guò)墓懂,這個(gè)在類加載的時(shí)候就立即初始化,因?yàn)樗莛I嘛霉囚,一開始就給你創(chuàng)建一個(gè)對(duì)象捕仔,這個(gè)是絕對(duì)線程安全的,在線程還沒出現(xiàn)以前就實(shí)例化了盈罐,不可能存在訪問(wèn)安全問(wèn)題榜跌。他的缺點(diǎn)是如果不用,用不著暖呕,我都占著空間斜做,造成內(nèi)存浪費(fèi)。
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
還有一種是餓漢式的變種湾揽,靜態(tài)代碼塊寫法瓤逼,原理也是一樣,只要是靜態(tài)的库物,在類加載的時(shí)候就已經(jīng)成功初始化了霸旗,這個(gè)和上面的比起來(lái)沒什么區(qū)別,無(wú)非就是裝個(gè)b戚揭,看起來(lái)比上面那種吊诱告,因?yàn)橐娺^(guò)的人不多嘛。
public class HungryStaticSingleton {
private static final HungryStaticSingleton hungrySingleton;
static {
hungrySingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton() {
}
public static HungryStaticSingleton getInstance() {
return hungrySingleton;
}
}
2民晒、懶漢式
簡(jiǎn)單懶漢
為了解決餓漢式占著茅坑不拉屎的問(wèn)題精居,就產(chǎn)生了下面這種簡(jiǎn)單懶漢式的寫法,一開始我先申明個(gè)對(duì)象潜必,但是先不創(chuàng)建他靴姿,當(dāng)用到的時(shí)候判斷一下是否為空,如果為空我就創(chuàng)建一個(gè)對(duì)象返回磁滚,如果不為空則直接返回佛吓。
為什么叫懶漢式,就是因?yàn)樗軕邪〈谷粒扔玫降臅r(shí)候才去創(chuàng)建维雇,看上去很ok,但是在多線程的情況下會(huì)產(chǎn)生線程安全問(wèn)題晒他。
public class LazySimpleSingleton {
private static LazySimpleSingleton instance;
private LazySimpleSingleton() {
}
public static LazySimpleSingleton getInstance() {
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
如果有兩個(gè)線程同時(shí)執(zhí)行到 if (instance==null) 這行代碼吱型,這是判斷都會(huì)通過(guò),然后各自會(huì)執(zhí)行instance = new Singleton()陨仅,并各自返回一個(gè)instance唁影,這時(shí)候就產(chǎn)生了多個(gè)實(shí)例耕陷,就沒有保證單例,如下圖所示据沈。
怎么解決這個(gè)問(wèn)題呢哟沫,很簡(jiǎn)單,加鎖啊锌介,加一下synchronized即可嗜诀,這樣就能保住線程安全問(wèn)題了。
3孔祸、雙重校驗(yàn)鎖(DCL)
上面這樣寫法帶來(lái)一個(gè)缺點(diǎn)隆敢,就是性能低,只有在第一次進(jìn)行初始化的時(shí)候才需要進(jìn)行并發(fā)控制崔慧,而后面進(jìn)來(lái)的請(qǐng)求不需要在控制了拂蝎,現(xiàn)在synchronized加在方法上,我管你生成沒成生成惶室,只要來(lái)了就得給我排隊(duì)温自,所以這種性能是極其低下的,那怎么辦呢皇钞?
我們知道悼泌,其實(shí)synchronized除了加在方法上,還可以加在代碼塊上夹界,只要對(duì)生成對(duì)象的那一部分代碼加鎖就可以了馆里,由此產(chǎn)生一種新的寫法,叫做雙重檢驗(yàn)鎖可柿,我們看下面代碼鸠踪。
我們看19行將synchronized包在了代碼塊上,當(dāng) singleton == null 的時(shí)候复斥,我們只對(duì)創(chuàng)建對(duì)象這一塊邏輯進(jìn)行了加鎖控制营密,如果 singleton != null 的話,就直接返回永票,大大提升了效率。
在21行的時(shí)候又加了一個(gè)singleton == null滥沫,這又是為什么呢侣集,原因是如果兩個(gè)線程都到了18行,發(fā)現(xiàn)是空的兰绣,然后都進(jìn)入到代碼塊世分,這里雖然加了synchronized,但作用只是進(jìn)行one by one串行化缀辩,第一個(gè)線程往下走創(chuàng)建了對(duì)象臭埋,第二個(gè)線程等待第一個(gè)線程執(zhí)行完畢后踪央,我也往下走,于是乎又創(chuàng)建了一個(gè)對(duì)象瓢阴,那還是沒控制住單例畅蹂,所以在21行當(dāng)?shù)诙€(gè)線程往下走的時(shí)候在判斷一次,是不是被別的線程已經(jīng)創(chuàng)建過(guò)了荣恐,這個(gè)就是雙重校驗(yàn)鎖液斜,進(jìn)行了兩次非空判斷。
我們看到在11行的時(shí)候加了 volatile 關(guān)鍵字叠穆,這是用來(lái)防止指令重排的少漆,當(dāng)我們創(chuàng)建對(duì)象的時(shí)候會(huì)經(jīng)過(guò)下面幾個(gè)步驟,但是這幾個(gè)步驟不是原子的硼被,計(jì)算機(jī)比較聰明示损,有時(shí)候?yàn)榱颂岣咝仕皇前错樞?234執(zhí)行的,可能是3214執(zhí)行嚷硫。
這時(shí)候如果第一個(gè)線程執(zhí)行了instance = new LazyDoubleCheckSingleton()检访,由于指令重排先進(jìn)行了第三步,先分配了一個(gè)內(nèi)存地址论巍,第二個(gè)線程進(jìn)來(lái)的時(shí)候發(fā)現(xiàn)對(duì)象已經(jīng)是非null烛谊,直接返回,但這時(shí)候?qū)ο筮€沒初始化好啊嘉汰,第二個(gè)線程拿到的是一個(gè)沒有初始化好的對(duì)象丹禀!這個(gè)就是要加volatile的原因。
- 分配內(nèi)存給這個(gè)對(duì)象
- 初始化對(duì)象
- 設(shè)置instance指向剛分配的內(nèi)存地址
- 初次訪問(wèn)對(duì)象
最后說(shuō)下雙重校驗(yàn)鎖鞋怀,雖然提高了性能双泪,但是在我看來(lái)不夠優(yōu)雅,折騰來(lái)折騰去密似,一會(huì)防這一會(huì)防那焙矛,尤其是對(duì)新手不友好,新手會(huì)不明白為什么要這么寫残腌。
4村斟、靜態(tài)內(nèi)部類
上面已經(jīng)將鎖的粒度縮小到創(chuàng)建對(duì)象的時(shí)候了,但不管加在方法上還是加在代碼塊上抛猫,終究還是用到了鎖蟆盹,只要用到鎖就會(huì)產(chǎn)生性能問(wèn)題,那有沒有不用鎖的方式呢闺金?
答案是有的逾滥,那就是靜態(tài)內(nèi)部類的方式,他其實(shí)是利用了java代碼的一種特性败匹,靜態(tài)內(nèi)部類在主類加載的時(shí)候是不會(huì)被加載的寨昙,只有當(dāng)調(diào)用getInstance()方法的時(shí)候才會(huì)被加載進(jìn)來(lái)進(jìn)行初始化讥巡,代碼如下
/**
* @author jack xu
* 兼顧餓漢式的內(nèi)存浪費(fèi),也兼顧synchronized性能問(wèn)題
*/
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
}
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
}
}
好舔哪,講到這里我已經(jīng)介紹了五種單例的寫法欢顷,經(jīng)過(guò)層層的演進(jìn)推理,到第五種的時(shí)候已經(jīng)是很完美的寫法了尸红,既兼顧餓漢式的內(nèi)存浪費(fèi)吱涉,也兼顧synchronized性能問(wèn)題。
那他真的一定完美嗎外里,其實(shí)不然怎爵,他還有一個(gè)安全的問(wèn)題,接下來(lái)我們講下單例的破壞盅蝗,有兩種方式反射和序列化鳖链。
單例的破壞
1、反射
我們知道在上面單例的寫法中墩莫,在構(gòu)造方法上加上private關(guān)鍵字修飾芙委,就是為了不讓外部通過(guò)new的方式來(lái)創(chuàng)建對(duì)象,但還有一種暴力的方法狂秦,我就是不走尋常路灌侣,你不讓我new是吧,我反射給你創(chuàng)建出來(lái)裂问,代碼如下
/**
* @author jack xu
*/
public class ReflectDestroyTest {
public static void main(String[] args) {
try {
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面c.setAccessible(true)就是強(qiáng)吻侧啼,你private了,我現(xiàn)在把你的權(quán)限設(shè)為true堪簿,我照樣能夠訪問(wèn)痊乾,通過(guò)c.newInstance()調(diào)用了兩次構(gòu)造方法,相當(dāng)于new了兩次椭更,我們知道 == 比的是地址哪审,最后結(jié)果是false,確實(shí)是創(chuàng)建了兩個(gè)對(duì)象虑瀑,反射破壞單例成功湿滓。
那么如何防止反射呢,很簡(jiǎn)單舌狗,就是在構(gòu)造方法中加一個(gè)判斷
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
if (LazyHolder.INSTANCE != null) {
throw new RuntimeException("不要試圖用反射破壞單例模式");
}
}
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton();
}
}
在看結(jié)果叽奥,防止反射成功,當(dāng)調(diào)用構(gòu)造方法時(shí)把夸,發(fā)現(xiàn)單例實(shí)例對(duì)象已經(jīng)不為空了而线,拋出異常铭污,不讓你在繼續(xù)創(chuàng)建了恋日。
2膀篮、序列化
接下來(lái)介紹單例的另一種破壞方式,先在靜態(tài)內(nèi)部類上實(shí)現(xiàn)Serializable接口岂膳,然后寫個(gè)測(cè)試方法測(cè)試下誓竿,先創(chuàng)建一個(gè)對(duì)象,然后把這個(gè)對(duì)象先序列化谈截,然后在反序列化出來(lái)筷屡,然后對(duì)比一下
public static void main(String[] args) {
LazyInnerClassSingleton s1 = null;
LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (LazyInnerClassSingleton) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
我們來(lái)看結(jié)果發(fā)現(xiàn)是false,在進(jìn)行反序列化時(shí)簸喂,在ObjectInputStream的readObject生成對(duì)象的過(guò)程中毙死,其實(shí)會(huì)通過(guò)反射的方式調(diào)用無(wú)參構(gòu)造方法新建一個(gè)對(duì)象,所以反序列化后的對(duì)象和手動(dòng)創(chuàng)建的對(duì)象是不一致的喻鳄。
那么怎么避免呢扼倘,依然很簡(jiǎn)單,在靜態(tài)內(nèi)部類里加一個(gè)readResolve方法即可
private Object readResolve() {
return LazyHolder.INSTANCE;
}
在看結(jié)果就變成true了除呵,為什么加了一個(gè)方法就可以避免被序列化破壞呢再菊,這里不在展開,感興趣的小伙伴可以看下ObjectInputStream的readObject()方法颜曾,一步步往下走纠拔,會(huì)發(fā)現(xiàn)最終會(huì)調(diào)用readResolve()方法。
至此泛豪,史上最牛b單例產(chǎn)生稠诲,已經(jīng)無(wú)懈可擊、無(wú)可挑剔了候址。
3吕粹、枚舉
那么這里我為什么還要在介紹枚舉呢,在《Effective Java》中岗仑,枚舉是被推薦的一種方式匹耕,因?yàn)樗銐蚝?jiǎn)單,線程安全荠雕,也不會(huì)被反射和序列化破壞稳其。
大家看下才寥寥幾句話,不像上面雖然已經(jīng)實(shí)現(xiàn)了最牛b的寫法炸卑,但是其中的過(guò)程很讓人煩惱啊既鞠,要考慮性能、內(nèi)存盖文、線程安全嘱蛋、破壞啊,一會(huì)這里加代碼一會(huì)那里加代碼,才能達(dá)到最終的效果洒敏。
而使用枚舉龄恋,感興趣的小伙伴可以反編譯看下,枚舉的底層其實(shí)還是一個(gè)class類凶伙,而我們考慮的這些問(wèn)題JDK源碼其實(shí)幫我們都已經(jīng)實(shí)現(xiàn)好了郭毕,所以在 java 層面我們只需要用三句話就能搞定!
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
至此函荣,我通過(guò)層層演進(jìn)显押,由淺入深的給大家介紹了單例的這么多寫法,從不完美到完美傻挂,這么多也是網(wǎng)上很常見的寫法乘碑,下面我在送大家兩個(gè)彩蛋,擴(kuò)展一下其他寫單例的方式方法金拒。
彩蛋
1蝉仇、容器式單例
容器式單例是我們 spring 中管理單例的模式,我們平時(shí)在項(xiàng)目中會(huì)創(chuàng)建很多的Bean殖蚕,當(dāng)項(xiàng)目啟動(dòng)的時(shí)候spring會(huì)給我們管理轿衔,幫我們加載到容器中,他的思路方式方法如下睦疫。
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className) {
Object instance = null;
if (!ioc.containsKey(className)) {
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
} else {
return ioc.get(className);
}
}
}
這個(gè)可以說(shuō)是一個(gè)簡(jiǎn)易版的 spring 管理容器害驹,大家看下這里用一個(gè)map來(lái)保存對(duì)象,當(dāng)對(duì)象存在的時(shí)候直接從map里取出來(lái)返回出去蛤育,如果不存在先用反射創(chuàng)建一個(gè)對(duì)象出來(lái)宛官,先保存到map中然后在返回出去。我們來(lái)測(cè)試一下瓦糕,先創(chuàng)建一個(gè)Pojo對(duì)象底洗,然后兩次從容器中去取出來(lái),比較一下咕娄,發(fā)現(xiàn)結(jié)果是true亥揖,證明兩次取出的對(duì)象是同一個(gè)對(duì)象。
但是這里有一個(gè)問(wèn)題圣勒,這樣的寫法是線程不安全的费变,那么如何做到線程安全呢,這個(gè)留給小伙伴自行獨(dú)立思考完成圣贸。
2挚歧、CAS單例
從一道面試題開始:不使用synchronized和lock,如何實(shí)現(xiàn)一個(gè)線程安全的單例吁峻?我們知道滑负,上面講過(guò)的所有方式中在张,只要是線程安全的,其實(shí)都直接或者間接用到了synchronized矮慕,間接用到是什么意思呢瞧掺,就比如餓漢式、靜態(tài)內(nèi)部類凡傅、枚舉,其實(shí)現(xiàn)原理都是利用借助了類加載的時(shí)候初始化單例肠缔,即借助了ClassLoader的線程安全機(jī)制夏跷。
所謂ClassLoader的線程安全機(jī)制,就是ClassLoader的loadClass方法在加載類的時(shí)候使用了synchronized關(guān)鍵字明未。也正是因?yàn)檫@樣槽华, 除非被重寫,這個(gè)方法默認(rèn)在整個(gè)裝載過(guò)程中都是同步的趟妥,也就是保證了線程安全猫态。
那么答案是什么呢,就是利用CAS樂(lè)觀鎖披摄,他雖然名字中有個(gè)鎖字亲雪,但其實(shí)是無(wú)鎖化技術(shù),當(dāng)多個(gè)線程嘗試使用CAS同時(shí)更新同一個(gè)變量時(shí)疚膊,只有其中一個(gè)線程能更新變量的值义辕,而其它線程都失敗,失敗的線程并不會(huì)被掛起寓盗,而是被告知這次競(jìng)爭(zhēng)中失敗灌砖,并可以再次嘗試,代碼如下:
/**
* @author jack xu
*/
public class CASSingleton {
private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<CASSingleton>();
private CASSingleton() {
}
public static CASSingleton getInstance() {
for (; ; ) {
CASSingleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new CASSingleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
在JDK1.5中新增的JUC包就是建立在CAS之上的傀蚌,相對(duì)于對(duì)于synchronized這種阻塞算法智末,CAS是非阻塞算法的一種常見實(shí)現(xiàn)爽航,他是一種基于忙等待的算法,依賴底層硬件的實(shí)現(xiàn),相對(duì)于鎖它沒有線程切換和阻塞的額外消耗爷狈,可以支持較大的并行度。
雖然CAS沒有用到鎖缴饭,但是他在不停的自旋奥帘,會(huì)對(duì)CPU造成較大的執(zhí)行開銷,在生產(chǎn)中我們不建議使用舅桩,那么為什么我還會(huì)講呢酱虎,因?yàn)檫@是工作擰螺絲,面試造火箭的典型擂涛!你可以不用读串,但是你得知道聊记,你說(shuō)是吧。
作者:jack_xu
鏈接:https://juejin.im/post/5ed5c50af265da76d3187b30
文源網(wǎng)絡(luò)恢暖,僅供學(xué)習(xí)之用排监,如有侵權(quán)請(qǐng)聯(lián)系刪除。
我將面試題和答案都整理成了PDF文檔杰捂,還有一套學(xué)習(xí)資料舆床,涵蓋Java虛擬機(jī)、spring框架嫁佳、Java線程挨队、數(shù)據(jù)結(jié)構(gòu)、設(shè)計(jì)模式等等蒿往,但不僅限于此盛垦。
關(guān)注公眾號(hào)【java圈子】獲取資料,還有優(yōu)質(zhì)文章每日送達(dá)瓤漏。