模式介紹
單例模式是應(yīng)用最廣泛的模式之一颜说。
單例模式是為了確保一個(gè)類(lèi)在整個(gè)項(xiàng)目中只有一個(gè)實(shí)例對(duì)象。
單例模式最大的優(yōu)勢(shì)就是可以避免資源的浪費(fèi)啸澡。
比如訪問(wèn)IO和數(shù)據(jù)庫(kù)等資源時(shí)就應(yīng)考慮使用單例模式飞崖。
模式特點(diǎn)
構(gòu)造方法私有化,使用
private
來(lái)修飾;確保對(duì)象有且只有一個(gè)铲咨,尤其是在多線程的環(huán)境下躲胳;
通過(guò)靜態(tài)方法或枚舉返回已經(jīng)實(shí)例化好的對(duì)象。
模式示例
實(shí)現(xiàn)單例模式的方式有很多纤勒,不過(guò)核心是不變的坯苹,都要嚴(yán)格遵循單例模式的特點(diǎn)。
下面我們來(lái)介紹實(shí)現(xiàn)單例的方式:
1. 餓漢式
public class 餓漢式 {
//自行實(shí)例化對(duì)象
private static final 餓漢式 ourInstance = new 餓漢式();
//通過(guò)靜態(tài)方法返回對(duì)象
public static 餓漢式 getInstance() {
return ourInstance;
}
//構(gòu)造方法私有化摇天,不能通過(guò)new來(lái)創(chuàng)建對(duì)象
private 餓漢式() {
}
}
值得一提的是粹湃,AndroidStudio在創(chuàng)建類(lèi)時(shí)指定該類(lèi)為單例的時(shí)候,默認(rèn)就是使用餓漢式:
餓漢式寫(xiě)起來(lái)非常簡(jiǎn)單泉坐、快捷再芋,但是缺點(diǎn)也顯而易見(jiàn):
在類(lèi)初始化的時(shí)候,對(duì)象就已經(jīng)創(chuàng)建好了坚冀。
如果說(shuō)我們沒(méi)有用到該類(lèi)济赎,就會(huì)造成資源的浪費(fèi)。
2. 懶漢式
public class 最簡(jiǎn)單的懶漢式 {
//全項(xiàng)目唯一的對(duì)象
private static 最簡(jiǎn)單的懶漢式 ourInstance;
//構(gòu)造方法私有化
private 最簡(jiǎn)單的懶漢式() {
}
//通過(guò)靜態(tài)方法來(lái)返回對(duì)象
public static 最簡(jiǎn)單的懶漢式 getInstance() {
//在調(diào)用該方法時(shí)進(jìn)行判空记某,在對(duì)象為null時(shí)創(chuàng)建對(duì)象
if (ourInstance == null) {
ourInstance = new 最簡(jiǎn)單的懶漢式();
}
return ourInstance;
}
}
這就是單例中懶漢式的最基本寫(xiě)法司训。
比起餓漢式,最大的優(yōu)勢(shì)就是不會(huì)造成資源的浪費(fèi)液南。因?yàn)橹挥性谟玫綍r(shí)壳猜,才會(huì)進(jìn)行對(duì)象的實(shí)例化。
但是就上面的寫(xiě)法而言滑凉,還存在一個(gè)很致命的問(wèn)題:
在多線程同時(shí)調(diào)用時(shí)统扳,會(huì)出現(xiàn)多個(gè)實(shí)例對(duì)象的情況。
Demo里有對(duì)應(yīng)的測(cè)試代碼畅姊,出現(xiàn)的概率很小咒钟,但是確實(shí)會(huì)出現(xiàn)。
解決這個(gè)問(wèn)題的方式也很簡(jiǎn)單若未,為靜態(tài)方法添加同步鎖:
//通過(guò)靜態(tài)方法來(lái)返回對(duì)象
public static synchronized 同步鎖的懶漢式 getInstance() {
//在調(diào)用該方法時(shí)進(jìn)行判空朱嘴,在對(duì)象為null時(shí)創(chuàng)建對(duì)象
if (ourInstance == null) {
ourInstance = new 同步鎖的懶漢式();
}
return ourInstance;
}
synchronized
就是同步鎖的關(guān)鍵字,加上該關(guān)鍵字粗合,代表著該方法同時(shí)只能在唯一的一個(gè)線程中運(yùn)行萍嬉。
比如當(dāng)10個(gè)線程去調(diào)用同步鎖的懶漢式.getInstance()
時(shí),只有當(dāng)?shù)?個(gè)線程完成訪問(wèn)時(shí)隙疚,第2個(gè)線程才會(huì)開(kāi)始執(zhí)行該方法壤追。當(dāng)?shù)?個(gè)線程訪問(wèn)完成后,單例對(duì)象就已經(jīng)創(chuàng)建完成供屉,所以第2個(gè)線程就會(huì)直接返回該對(duì)象行冰,不會(huì)再去創(chuàng)建捅厂,這就保證了線程安全。
這樣確實(shí)解決了我們所說(shuō)的線程安全的問(wèn)題资柔,但是這種做法明顯是低效率的:
我們的目的是保證項(xiàng)目中有且只有一個(gè)對(duì)象焙贷,上述代碼確實(shí)實(shí)現(xiàn)了這個(gè)目的。
但是當(dāng)對(duì)象創(chuàng)建成功后贿堰,我們希望多線程訪問(wèn)的時(shí)候應(yīng)該是異步高效辙芍、同時(shí)執(zhí)行的的,而不是像上面那樣隊(duì)列式的羹与,我要等你用完我才能用故硅。所以就有了雙重校驗(yàn)鎖的懶漢式:
public static 同步鎖的懶漢式 getInstance() {
if (ourInstance == null) {
synchronized (new Object()) {
if (ourInstance == null) {
ourInstance = new 同步鎖的懶漢式()
}
}
}
return ourInstance;
}
這種寫(xiě)法可以完美解決多線程效率低下的問(wèn)題,那么到底是如何解決的纵搁?
雙重校驗(yàn)鎖指的是會(huì)進(jìn)行兩次判空操作:
ourInstance == null
一次在同步鎖外吃衅,一次在同步鎖內(nèi)。
有的看官就有疑問(wèn)了:兩次判空腾誉?
首先是synchronized
關(guān)鍵字徘层,我們刪除了方法的同步鎖,將其移動(dòng)到了方法內(nèi)部利职,對(duì)ourInstance = new 同步鎖的懶漢式()
單獨(dú)加鎖趣效。
這就代表著我們這個(gè)方法本身已經(jīng)不是線程安全了,會(huì)有多個(gè)線程同時(shí)訪問(wèn)外層的if猪贪。如果同步鎖內(nèi)部沒(méi)有判空跷敬,就會(huì)有多個(gè)線程等待對(duì)象創(chuàng)建,就會(huì)生成多個(gè)實(shí)例對(duì)象热押。
所以雙重校驗(yàn)鎖的每一步都非常關(guān)鍵西傀,必不可少。
雙重校驗(yàn)鎖的寫(xiě)法主要是為了在多線程創(chuàng)建對(duì)象時(shí)桶癣,用同步鎖來(lái)保證對(duì)象的唯一拥褂。當(dāng)對(duì)象創(chuàng)建完成后,同步鎖外層的判空操作就不成立了鬼廓,那么會(huì)直接返回對(duì)象肿仑,整個(gè)方法就與同步鎖無(wú)關(guān)致盟,多線程訪問(wèn)時(shí)也就不需要等待了碎税。
雙重校驗(yàn)鎖懶漢式,看起來(lái)已經(jīng)非常完美了馏锡!
但是雷蹂,很遺憾。
因?yàn)镴VM存在指令重排的優(yōu)化杯道,又會(huì)產(chǎn)生新的問(wèn)題匪煌。
指令重排是JVM為了提高程序運(yùn)行效率责蝠。
JVM規(guī)范規(guī)定,指令重排序可以在不影響單線程程序執(zhí)行結(jié)果的情況下改變代碼執(zhí)行順序萎庭。
該處會(huì)產(chǎn)生指令重排的代碼是
ourInstance = new 同步鎖的懶漢式();
這句代碼在JVM看來(lái)霜医,主要是做了以下三件事情:
(1)給ourInstance
分配內(nèi)存;
(2)調(diào)用構(gòu)造方法創(chuàng)建對(duì)象驳规,對(duì)對(duì)象進(jìn)行初始化肴敛;
(3)將ourInstance
對(duì)象指向JVM分配的內(nèi)存空間(此步完成之后,ourInstance
就是非null了)吗购。
因?yàn)镴VM存在指令重排医男,所以在不影響最終結(jié)果的情況下,JVM會(huì)選擇性能最優(yōu)的的順序執(zhí)行:
也就是說(shuō)捻勉,上面三件事情镀梭,執(zhí)行的順序可能是1-2-3,也有可能是1-3-2踱启。
1-2-3报账,1-3-2,有區(qū)別嗎埠偿?
在結(jié)果上來(lái)看笙什,沒(méi)有任何區(qū)別。
但是在多線程的情況下胚想,是有風(fēng)險(xiǎn)的:
假設(shè)線程x的執(zhí)行順序是1-3-2琐凭,當(dāng)3執(zhí)行完成時(shí),ourInstance
就已經(jīng)不為空了浊服,但是2還沒(méi)有執(zhí)行完成時(shí)统屈,線程y介入了。此時(shí)線程y會(huì)發(fā)現(xiàn)ourInstance
已經(jīng)不為null了牙躺,但是其實(shí)ourInstance
的初始化工作并未完成愁憔,這樣很明顯就會(huì)產(chǎn)生異常。
解決方法也非常簡(jiǎn)單孽拷,利用volatile
關(guān)鍵字即可:
public class 完美的懶漢式 {
//全項(xiàng)目唯一的對(duì)象
//volatile關(guān)鍵字吨掌,禁止指令重排
private volatile static 完美的懶漢式 ourInstance;
//構(gòu)造方法私有化
private 完美的懶漢式() {
}
//通過(guò)靜態(tài)方法來(lái)返回對(duì)象
public static 完美的懶漢式 getInstance() {
//在調(diào)用該方法時(shí)進(jìn)行判空,在對(duì)象為null時(shí)創(chuàng)建對(duì)象
if (ourInstance == null) {
synchronized (new Object()) {
if (ourInstance == null) {
ourInstance = new 完美的懶漢式();
}
}
}
return ourInstance;
}
上述代碼就是一個(gè)完美的懶漢式了脓恕,利用volatile
關(guān)鍵字來(lái)禁止JVM的指令重排膜宋。
3. 枚舉(Enum)
public enum 枚舉單例 {
INSTANCE;
public String getUrl(){
return "http://www.baidu.com";
}
}
使用起來(lái)也非常簡(jiǎn)單:
String url = 枚舉單例.INSTANCE.getUrl();
簡(jiǎn)直完美啊炼幔!簡(jiǎn)單易用秋茫,代碼清晰!
總結(jié)
簡(jiǎn)單回顧一下:
單例模式是保證了一個(gè)類(lèi)在一個(gè)項(xiàng)目中有且只有一個(gè)實(shí)例對(duì)象乃秀。
這樣做的目的是為了節(jié)省內(nèi)存的開(kāi)支肛著。
單例模式的寫(xiě)法主要有:
- 項(xiàng)目初始化時(shí)就創(chuàng)建好的餓漢式圆兵;
- 在第一次使用時(shí)才進(jìn)行創(chuàng)建、但要注意線程安全的懶漢式枢贿;
- 使用非常簡(jiǎn)單的枚舉殉农。
感謝
Jark's Blog-如何正確地寫(xiě)出單例模式
《Android源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)》 何紅輝、關(guān)愛(ài)民 著