單例模式是經(jīng)典的設(shè)計(jì)模式之一。什么是設(shè)計(jì)模式?代碼的設(shè)計(jì)模式類(lèi)似于棋譜,棋譜就是一些下棋的固定套路,是前人總結(jié)出來(lái)的一些固定的打法。依照棋譜來(lái)下棋,不說(shuō)能下得非常好,但至少是有跡可循,不會(huì)下得很糟糕。代碼的設(shè)計(jì)模式也是一樣。
設(shè)計(jì)模式,就是軟件開(kāi)發(fā)中的棋譜。一些編程界的大佬,針對(duì)一些常見(jiàn)情景總結(jié)出了一些代碼的“編寫(xiě)套路”。按照這樣的套路來(lái)寫(xiě)代碼,不說(shuō)能寫(xiě)得非常好,但也至少不會(huì)寫(xiě)得太糟糕。以前有一個(gè)大佬寫(xiě)了一本書(shū),名叫《討論二十三種設(shè)計(jì)模式》,這本書(shū)廣為流傳,這里的設(shè)計(jì)模式也就是我們上面說(shuō)到的。
事實(shí)上設(shè)計(jì)模式遠(yuǎn)不止“二十三種”。以下兩種設(shè)計(jì)模式經(jīng)常遇到:
- 單例模式
- 工廠(chǎng)模式
本文主要介紹單例模式。
目錄
一、什么是單例模式?
二、如何實(shí)現(xiàn)單例模式?
1、代碼實(shí)現(xiàn):餓漢模式
a.餓漢模式的構(gòu)造思路
b.總結(jié):餓漢模式代碼
2、代碼實(shí)現(xiàn):懶漢模式
三、線(xiàn)程安全問(wèn)題
1、懶漢模式--線(xiàn)程不安全,餓漢模式--線(xiàn)程安全
2、初步解決:懶漢模式的線(xiàn)程安全問(wèn)題
3、代碼問(wèn)題-1:加鎖導(dǎo)致程序效率低——解決:更改加鎖的位置
4、代碼問(wèn)題-2:new操作引發(fā)指令重排序——解決:以volatile修飾
四、***小結(jié):?jiǎn)卫J降木€(xiàn)程安全問(wèn)題
一、什么是單例模式?
單例指的就是單個(gè)實(shí)例(instance),也就是單個(gè)對(duì)象(對(duì)象就是類(lèi)的實(shí)例)。單例模式指的是某個(gè)類(lèi)在進(jìn)程中只有唯一一個(gè)實(shí)例(在一個(gè)程序中,只能創(chuàng)建一個(gè)實(shí)例(一個(gè)對(duì)象),不能創(chuàng)建多個(gè)對(duì)象)。
按理來(lái)說(shuō),在寫(xiě)代碼的時(shí)候多 new 幾次,就能創(chuàng)建多個(gè)對(duì)象了。但在語(yǔ)法上,是有辦法禁止這樣多 new 幾次的操作的。?
也就是說(shuō),Java中的單例模式,實(shí)際上是借助 Java 語(yǔ)法,保證某個(gè)類(lèi)只能夠創(chuàng)建出一個(gè)實(shí)例,而不能被new多次。
為什么會(huì)有這樣的用途?其實(shí)原因是很簡(jiǎn)單的:在有些場(chǎng)景下,本身它就要求某個(gè)概念是單例的。比如每個(gè)人只能同時(shí)擁有一個(gè)配偶。
二、如何實(shí)現(xiàn)單例模式?
Java實(shí)現(xiàn)單例模式的方式有很多種,這里我們主要介紹兩種寫(xiě)法:
- 餓漢模式(急迫)
- 懶漢模式(從容)
如何理解?餓漢模式?和?懶漢模式?呢?餓漢模式就好比每次吃完飯之后,立刻就把碗給洗了(主打的就是一個(gè)急迫);懶漢模式則是每次吃完飯了,先把碗放到一邊先不洗,等到吃下一頓了再洗。通常認(rèn)為,懶漢模式更好,效率更高(非必要不洗碗)。
比如,中午吃飯用了4個(gè)碗,那么餓漢模式就得一次性把4個(gè)碗都洗了;而晚上吃飯要用2個(gè)碗,懶漢模式就只需要洗4個(gè)碗當(dāng)中用不到的2個(gè)碗就行了。洗2個(gè)碗明顯要比洗4個(gè)碗效率更高(不考慮沒(méi)洗的碗會(huì)變臭~(yú)~只考慮效率)。
在計(jì)算機(jī)中的例子:打開(kāi)一個(gè)硬盤(pán)上的文件,讀取文件內(nèi)容并顯示出來(lái)。
- 餓漢:把文件所有內(nèi)容都讀到內(nèi)存中,并顯示。
- 懶漢:只把文件讀一小部分,把當(dāng)前屏幕填充上。如果用戶(hù)翻頁(yè)了,再讀其它文件內(nèi)容;如果不翻頁(yè),就省下了。
在這樣的情況下,懶漢模式也是完勝餓漢模式的。
假設(shè)要讀取的文件非常大,有 10G,按照餓漢模式的方式,文件打開(kāi)可能都要卡半天,更何況還有內(nèi)存是否足夠的問(wèn)題。
但懶漢模式下就可以很快速地打開(kāi),因?yàn)樗蛔x取 1 頁(yè)的內(nèi)容,1 頁(yè)也就幾百字,可能也就讀取 2k 就夠了;如果用戶(hù)還要讀其它頁(yè)的內(nèi)容,懶漢再?gòu)膬?nèi)存里讀取相應(yīng)的內(nèi)容,用戶(hù)瀏覽不到的頁(yè)面,也就不將它的內(nèi)容加載到內(nèi)存中了。
(雖然懶漢模式會(huì)增加硬盤(pán)的讀取次數(shù),但和餓漢模式的情況相比,是不值一提的。)
下面我們來(lái)看看如何用代碼實(shí)現(xiàn)這兩種單例模式。
1、代碼實(shí)現(xiàn):餓漢模式
a.餓漢模式的構(gòu)造思路
我們先初步地創(chuàng)建出?Singleton 類(lèi),并在里面把對(duì)象創(chuàng)建出來(lái):
// 把一個(gè)類(lèi)設(shè)置成單例的
class Singleton {
// 唯一實(shí)例的本體
private static Singleton instance = new Singleton(); // 把對(duì)象創(chuàng)建出來(lái)
// 獲取到實(shí)例的方法
public static Singleton getInstance() {
return instance;
}
}
注意:這里的 instance 屬性要用 static 修飾,static變量保存了單例對(duì)象的唯一實(shí)例。
同時(shí),將 instance 屬性用private封裝,并提供一個(gè)get方法。這樣,我們就可以從外部獲取instance了:
public class Test {
public static void main(String[] args) {
// 此時(shí) s1 和 s2 是同一個(gè)對(duì)象
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
}
}
顯然,此處的 s1 和 s2 獲取到的實(shí)際是同一個(gè)對(duì)象。
但是,上述的代碼并沒(méi)有限定再次 new 對(duì)象的操作:
此處的 s3 也顯然與 s1 和 s2 不是同一個(gè)對(duì)象。因此,此處必須把 new 操作給禁止掉。采用的方式是 構(gòu)造方法私有化。
將構(gòu)造方法用 private 修飾,可以發(fā)現(xiàn),此時(shí)我們上面的 new 操作就報(bào)錯(cuò)了,無(wú)法通過(guò)編譯。
有些同學(xué)可能會(huì)想到,用反射仍然可以獲取到私有方法。一方面,反射本身就是一種非常規(guī)的手段,它本身就是不安全的;另一方面,單例模式有一種實(shí)現(xiàn)方式,借助枚舉,也可以保證反射下的安全,這個(gè)在此不過(guò)多介紹。
b.總結(jié):餓漢模式代碼
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton(){ }
}
public class Test {
public static void main(String[] args) {
//此時(shí)s1和s2是同一個(gè)對(duì)象
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
}
}

通過(guò)Java語(yǔ)法來(lái)限制類(lèi)實(shí)例的多次創(chuàng)建,從而實(shí)現(xiàn)單例模式:
- static是用于類(lèi)級(jí)別的數(shù)據(jù)共享,保存了單例對(duì)象的唯一實(shí)例,可以在單例模式中用來(lái)單例實(shí)例的唯一性。
- 單例模式中將構(gòu)造方法私有化,可以避免外部直接創(chuàng)建新的實(shí)例。
但餓漢模式的有一個(gè)問(wèn)題,那就是實(shí)例的創(chuàng)建時(shí)機(jī)過(guò)早了。只要類(lèi)一加載,就會(huì)創(chuàng)建出這個(gè)實(shí)例,可要是后面并沒(méi)有用到這個(gè)實(shí)例呢?
更好的實(shí)現(xiàn)方式是懶漢模式。
2、代碼實(shí)現(xiàn):懶漢模式
懶漢模式的核心思想:非必要,不創(chuàng)建。懶漢模式和餓漢模式的代碼實(shí)現(xiàn)類(lèi)似,最大的區(qū)別是餓漢模式在不使用instance對(duì)象時(shí),不把它new出來(lái)。
以下代碼就是懶漢模式的實(shí)現(xiàn):
class SingletonLazy {
//先令instance引用為null
private static SingletonLazy instance = null;
//獲取instance實(shí)例
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
//構(gòu)造方法私有化
private SingletonLazy() { }
}
????

三、線(xiàn)程安全問(wèn)題
1、懶漢模式--線(xiàn)程不安全,餓漢模式--線(xiàn)程安全
在Java多線(xiàn)程編程中,非常重要的一個(gè)問(wèn)題就是線(xiàn)程安全問(wèn)題。上述提到的兩個(gè)代碼,是線(xiàn)程安全的嗎?即,多個(gè)線(xiàn)程下調(diào)用getInstance()是否會(huì)出現(xiàn)問(wèn)題?
結(jié)論是:餓漢模式是線(xiàn)程安全的,而懶漢模式不是線(xiàn)程安全的。?
比對(duì)線(xiàn)程不安全的原因:線(xiàn)程安全問(wèn)題及解決措施?
- 線(xiàn)程的搶占式執(zhí)行。
- 多個(gè)線(xiàn)程修改同一變量。
- 修改操作不是原子的。
- 內(nèi)存可見(jiàn)性問(wèn)題。
- 指令重排序。
這里,引起懶漢模式線(xiàn)程不安全的最直接原因,是多個(gè)線(xiàn)程修改同一變量。
在餓漢模式的getInstance()中,只是單純地讀操作(return),不涉及修改。而懶漢模式的getInstance()中有一個(gè)這樣的操作:先判定是否為null,再進(jìn)行修改,再返回。
很明顯,這里包含了修改的操作。上面的懶漢模式代碼在多線(xiàn)程下,可能無(wú)法保證創(chuàng)建對(duì)象的唯一性。如下圖情況中,t1和t2都會(huì)執(zhí)行到對(duì)象創(chuàng)建的代碼,從而創(chuàng)建出多份對(duì)象。
多創(chuàng)建一個(gè)對(duì)象,聽(tīng)起來(lái)似乎問(wèn)題不大,其實(shí)不然。對(duì)象是有大有小的,有些對(duì)象管理的內(nèi)存數(shù)據(jù)可能會(huì)很多,甚至可能多達(dá)幾百G。如果n個(gè)線(xiàn)程一起調(diào)用,創(chuàng)建出了n個(gè)這樣大的對(duì)象,后果是非常嚴(yán)重的。?
2、初步解決:懶漢模式的線(xiàn)程安全問(wèn)題
深入來(lái)說(shuō),引起上述問(wèn)題的原因是if判定操作與修改操作不是原子的。可以通過(guò)加鎖來(lái)解決這個(gè)問(wèn)題。
但是,考慮到多線(xiàn)程代碼的復(fù)雜性,不是在代碼中任意寫(xiě)個(gè)加鎖,就一定線(xiàn)程安全了。如下面代碼所示:將synchronized加在了new對(duì)象的操作上,且以類(lèi)對(duì)象作為鎖對(duì)象。這樣的加鎖方式是不可行的,因?yàn)樵a出現(xiàn)線(xiàn)程不安全原因就是因?yàn)閕f判定操作與new操作不是原子的,而只把鎖加載new操作上,并不能保證if判定操作和修改操作整體的原子性。
因此,應(yīng)該把if操作也放到鎖里,才能保證判定和new是一個(gè)原子操作。
//獲取instance實(shí)例
public static SingletonLazy getInstance() {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
當(dāng)然,也可以直接將鎖加在方法上,直接保證整個(gè)方法都是原子的。
//獲取instance實(shí)例
synchronized public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
3、代碼問(wèn)題-1:加鎖導(dǎo)致程序效率低——解決:更改加鎖的位置
在之前線(xiàn)程安全的篇章中提到過(guò),加鎖其實(shí)是一種非常低效的方式,因?yàn)榧渔i意味著會(huì)出現(xiàn)阻塞等待。事實(shí)上,應(yīng)該“非必要,不加鎖”。而我們上述的加鎖方式中存在一個(gè)問(wèn)題:不管什么時(shí)候調(diào)用getInstance(),都會(huì)觸發(fā)鎖的競(jìng)爭(zhēng)。?
然而其實(shí),此處的線(xiàn)程不安全只發(fā)生在首次創(chuàng)建對(duì)象這里。一旦對(duì)象new好了,后續(xù)再調(diào)用getInstance(),就是單純的讀操作,就沒(méi)有線(xiàn)程安全問(wèn)題了,也就沒(méi)必要再加鎖了。
怎么優(yōu)化呢?我們就需要針對(duì)加鎖再做一次判定:
什么時(shí)候需要加鎖?——對(duì)象為空的時(shí)候。?
因此,要再加一層if判斷,用于判斷需要加鎖的情況:
//獲取instance實(shí)例
public static SingletonLazy getInstance() {
// 這個(gè)條件用于判斷是否要加鎖
// 如果對(duì)象已經(jīng)有了,就不必加鎖了,此時(shí)本身就是線(xiàn)程安全的
if(instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
?注意這兩個(gè)if(instance == null)代碼的辨析:
并且,雖然這倆代碼是挨著的,但是實(shí)際上它們執(zhí)行的時(shí)機(jī)差別會(huì)很大。?
按照我們?cè)趩尉€(xiàn)程代碼中的理解,如果兩行代碼緊挨著,那么執(zhí)行的時(shí)候,這兩行代碼會(huì)被迅速執(zhí)行完,可以近似地看作它們是同一時(shí)機(jī)被執(zhí)行。
但是,在多線(xiàn)程且上述兩個(gè)if判斷間隔著一層synchronized加鎖的情況下,就不能簡(jiǎn)單地這樣理解了。
加鎖就可能導(dǎo)致線(xiàn)程阻塞,而等到線(xiàn)程阻塞被接觸時(shí),可能早已是“滄海桑田”。換句話(huà)說(shuō),這兩行代碼雖然看起來(lái)是相鄰的,但它們執(zhí)行的時(shí)間間隔可能會(huì)非常長(zhǎng)。雖然兩個(gè)條件代碼完全相同,但若調(diào)用的時(shí)間間隔長(zhǎng)了,判斷結(jié)果也可能會(huì)不同。
比如在一個(gè)線(xiàn)程執(zhí)行時(shí),剛開(kāi)始instance為null,第一個(gè)if判定成立,進(jìn)入外層if;接下來(lái)獲取鎖時(shí)卻發(fā)現(xiàn),鎖已經(jīng)被別的線(xiàn)程獲取了,那么這個(gè)線(xiàn)程此時(shí)就只能阻塞等待;等到這個(gè)線(xiàn)程結(jié)束阻塞、再往下走的時(shí)候,instance卻已經(jīng)被別的線(xiàn)程創(chuàng)建好了,不再為null,那么第二個(gè)條件判定就不成立了;該線(xiàn)程不會(huì)進(jìn)入第二層if,也就不會(huì)重復(fù)再new一個(gè)對(duì)象了。
4、代碼問(wèn)題-2:new操作引發(fā)指令重排序——解決:以volatile修飾
在之前線(xiàn)程安全的篇章中提到過(guò),指令重排序也可能導(dǎo)致線(xiàn)程不安全。new操作包括3個(gè)步驟:1、創(chuàng)建內(nèi)存;2、調(diào)用構(gòu)造方法;3、把地址賦值給引用。這其中就可能存在指令重排序:步驟2和步驟3的順序可以調(diào)換。
如果程序按照 1-3-2 的方式執(zhí)行new操作,就可能出現(xiàn)問(wèn)題:
若instance為null,當(dāng)t1線(xiàn)程執(zhí)行完1和3這兩個(gè)步驟后,線(xiàn)程突然被調(diào)度到t2;t2再去判定條件,但由于在t1中instance已經(jīng)獲取了內(nèi)存地址,因此instance非null,條件不成立,會(huì)直接返回實(shí)例的引用。此時(shí),t2拿到的是一個(gè)沒(méi)裝修過(guò)的毛坯房。
如果接下來(lái)t2繼續(xù)毛坯房的后續(xù)方法,可能都是將錯(cuò)就錯(cuò)了。
總而言之,這樣的線(xiàn)程調(diào)度時(shí)機(jī),可能導(dǎo)致t2拿到的實(shí)例是不完整的,從而就出現(xiàn)問(wèn)題了。雖然這個(gè)過(guò)程是一個(gè)極端小概率的情況,但在服務(wù)器高并發(fā)、大數(shù)據(jù)的情況下,一旦出問(wèn)題,后果仍然是非常嚴(yán)重的。
如何解決這個(gè)問(wèn)題?很簡(jiǎn)單,將instance加上volatile即可。volatile可以禁止指令重排序。
//加上volatile
volatile private static SingletonLazy instance = null;
//獲取instance實(shí)例
public static SingletonLazy getInstance() {
// 這個(gè)條件用于判斷是否要加鎖
// 如果對(duì)象已經(jīng)有了,就不必加鎖了,此時(shí)本身就是線(xiàn)程安全的
if(instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
補(bǔ)充:這里是否涉及到內(nèi)存可見(jiàn)性問(wèn)題是存疑的。內(nèi)存可見(jiàn)性問(wèn)題的發(fā)生是由于編譯器優(yōu)化掉了寄存器從內(nèi)存中l(wèi)oad的這一操作,從而使得每一次讀取數(shù)據(jù)的時(shí)并沒(méi)有真正從內(nèi)存中讀取,而是只從寄存器中讀取。在一個(gè)線(xiàn)程頻繁寫(xiě),一個(gè)線(xiàn)程頻繁讀的情況下,可能會(huì)出現(xiàn)內(nèi)存可見(jiàn)性的問(wèn)題。但是,上述代碼是否涉及“頻繁讀”?假設(shè)N個(gè)線(xiàn)程一起調(diào)用,是否就相當(dāng)于讀了N次,這樣不就會(huì)觸發(fā)編譯器的優(yōu)化操作?文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-454642.html
這其實(shí)是不一定的。因?yàn)槊恳粋€(gè)線(xiàn)程,會(huì)有自己的一套寄存器,這其中是否會(huì)出現(xiàn)內(nèi)存安全性問(wèn)題,是很難確定的。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-454642.html
四、***小結(jié):?jiǎn)卫J降木€(xiàn)程安全問(wèn)題
- 餓漢模式:天然就是安全的,只是讀操作。
- 懶漢模式:不安全的有讀操作,也有寫(xiě)操作。如何保證懶漢模式的線(xiàn)程安全問(wèn)題:
- 加鎖,把 if 和 new 變成原子操作。
- 雙重 if,減少不必要的加鎖操作。
- 使用 volatile 禁止指重排序,保證后續(xù)線(xiàn)程肯定拿到的是完整對(duì)象。
到了這里,關(guān)于Java多線(xiàn)程基礎(chǔ)-8:?jiǎn)卫J郊捌渚€(xiàn)程安全問(wèn)題的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!