一、單例模式簡(jiǎn)介
單例模式,是一種常用的軟件設(shè)計(jì)模式别渔。在它的核心結(jié)構(gòu)中只包含一個(gè)被稱為單例的特殊類。通過(guò)單例模式可以保證系統(tǒng)中惧互,應(yīng)用該模式的類一個(gè)類只有一個(gè)實(shí)例哎媚。即一個(gè)類只有一個(gè)對(duì)象實(shí)例。在java代碼中喊儡,通常new關(guān)鍵字創(chuàng)造出來(lái)的對(duì)象拨与,對(duì)系統(tǒng)的開(kāi)銷一般都挺大的。所以在某些情況下艾猜,單例的實(shí)現(xiàn)也是應(yīng)對(duì)系統(tǒng)優(yōu)化的一種解決辦法买喧。
二、單例模式的實(shí)現(xiàn)
常見(jiàn)的單例有這幾種實(shí)現(xiàn)
- 餓漢式
- 飽漢式
- 雙重校驗(yàn)
- 靜態(tài)內(nèi)部類
1匆赃、餓漢式
先來(lái)介紹餓漢式淤毛,餓漢式,顧名思義算柳,就是一進(jìn)入這個(gè)類低淡,該類的實(shí)例就被初始化完成了。接下來(lái)來(lái)看下代碼。
public class Demo {
private static Demo h = new Demo();
private Demo(){
}
public static Demo getInstance(){
return h;
}
}
代碼也和簡(jiǎn)單查牌,就是直接構(gòu)造一個(gè)私有的構(gòu)造器事期,然后在建立一個(gè)成員變量,順便實(shí)例化該類纸颜,在調(diào)用該類的getInstance方法,當(dāng)然前面也說(shuō)過(guò)是一進(jìn)入這個(gè)類绎橘,
該類的實(shí)例就被創(chuàng)建完成胁孙,所以也可以利用類的加載順序來(lái)編寫這個(gè)代碼。比如 A類是B類的子類称鳞,在初始化A類的實(shí)例的時(shí)候涮较,會(huì)先去父類B中去,看看有沒(méi)有靜態(tài)塊和靜態(tài)成員變量(靜態(tài)方法只有被調(diào)用時(shí)才會(huì)加載冈止,且只會(huì)被加載一次)狂票,若有則先去加載B類的靜態(tài)塊和靜態(tài)成員變量,再加載A類的熙暴。之后會(huì)去調(diào)用B的構(gòu)造器闺属,最后才會(huì)調(diào)用本類對(duì)應(yīng)的構(gòu)造器。
所以我們可以在靜態(tài)代碼塊中實(shí)例化周霉。如下代碼
public class Demo {
private static Demo h = null;
private Demo(){
}
static{
h = new Demo();
}
public static Demo getInstance(){
return h;
}
}
該種實(shí)現(xiàn)的單例是線程安全的掂器。當(dāng)然由于它會(huì)提前初始化,所以會(huì)提前占用一些系統(tǒng)資源俱箱。
2国瓮、飽漢式
飽漢式的構(gòu)造實(shí)例的時(shí)候與餓汗式相反,它只有在第一次需要的時(shí)候才會(huì)去構(gòu)造實(shí)例狞谱。具體實(shí)現(xiàn)代碼如下
public class Demo {
private static Demo h = null;
private Demo(){
}
public static Demo getInstance(){
if(h == null){
//1
h = new Demo();
//2
}
return h;
}
}
飽漢式最常見(jiàn)的的編寫方式就是上述代碼乃摹,對(duì)于剛了解單例模式的人來(lái)說(shuō),飽漢式就寫完了跟衅,不過(guò)在單線程環(huán)境也確實(shí)可以說(shuō)是寫完了孵睬,A線程在獲取實(shí)例,第一次獲取時(shí)与斤,看見(jiàn)為null肪康,進(jìn)行初始化,第二次撩穿,不是null磷支,直接返回。這也是一種很理想的流程食寡。但是值得注意的是雾狈,在多線程下,它就值得推敲了抵皱。比如看下面例子
- 線程A:嘿嘿善榛!我已經(jīng)走到了2辩蛋,這初始化的好處我就要獨(dú)占了,想想都雞凍移盆,我要去初始化Demo類的實(shí)例了悼院,啦啦啦。
- 線程B:哈哈咒循!線程A那個(gè)SD据途,我都走到了1,它竟然還沒(méi)發(fā)現(xiàn)我叙甸,還想獨(dú)占Demo的實(shí)例化颖医,沒(méi)門!裆蒸!
旁白:線程A還完成了對(duì)該類實(shí)例的初始化熔萧,線程B也進(jìn)入了對(duì)該實(shí)例的構(gòu)造中,
因此僚祷,線程A和線程B都同時(shí)初始化了該實(shí)例佛致,這也不滿足單例的條件。
于是有人很自然的想到久妆,加鎖晌杰。如下
public class Demo {
private static Demo h = null;
private Demo(){
}
public synchronized static Demo getInstance(){
if(h == null){
h = new Demo();
}
return h;
}
}
這樣確實(shí)可以防止多線程環(huán)境造成多個(gè)實(shí)例。但的缺點(diǎn)是每一次獲取都去加鎖筷弦,會(huì)對(duì)性能有一定的損失肋演。所以有了雙重校驗(yàn)鎖。
3烂琴、雙重校驗(yàn)
雙重校驗(yàn)爹殊,就是在獲取單例的時(shí)候,對(duì)加鎖的方式進(jìn)行了改變奸绷,它不在方法上加鎖梗夸,它對(duì)代碼塊進(jìn)行加鎖,這樣的效率比飽漢式要高号醉。具體代碼如下
public class Demo {
private static Demo h = null;
private Demo(){
}
public static Demo getInstance(){
if(h == null){
//1
synchronized (new Object()) {
//2
if(h == null){
h = new Demo();
}
}
}
return h;
}
}
也許有人看了以上代碼后會(huì)有疑問(wèn)反症,要加上兩個(gè)if判斷干嘛?加一個(gè)不行嗎畔派,比如如下
public class Demo {
private static Demo h = null;
private Demo(){
}
public static Demo getInstance(){
if(h == null){
//1
synchronized (new Object()) {
//2
h = new Demo();
}
}
return h;
}
}
這樣不也對(duì)進(jìn)行實(shí)例化的時(shí)候加鎖了嗎铅碍?也可以保證線程安全啊线椰!
看這個(gè)例子
- 線程A:嘿嘿胞谈!我已經(jīng)走到了2,咦!竟然還有鎖烦绳,更好了卿捎,這初始化的好處我就要獨(dú)占了,想想都雞凍径密,我要去初始化Demo類的實(shí)例了午阵,啦啦啦。
- 線程B:哈哈睹晒!線程A那個(gè)SD趟庄,我都走到了1,它竟然還沒(méi)發(fā)現(xiàn)我伪很,還想獨(dú)占Demo的實(shí)例化,沒(méi)門7艿ァ锉试!臥槽,靜態(tài)被線程A那個(gè)SD上鎖了览濒,哎等等吧呆盖!
- 線程B:咦!鎖的鑰匙又回來(lái)了贷笛,哎应又,沒(méi)希望了,希望能給我喝點(diǎn)湯乏苦。n秒后株扛,B處于驚訝中,沒(méi)想到我還能初始化汇荐。哈哈哈洞就。
為啥雙重會(huì)被認(rèn)為是線程安全的∠铺裕看這個(gè)例子
- 線程A:嘿嘿旬蟋!我已經(jīng)走到了2,咦革娄!竟然還有鎖倾贰,更好了,這初始化的好處我就要獨(dú)占了拦惋,想想都雞凍匆浙,我要去初始化Demo類的實(shí)例了,啦啦啦架忌。
- 線程B:哈哈吞彤!線程A那個(gè)SD,我都走到了1,它竟然還沒(méi)發(fā)現(xiàn)我饰恕,還想獨(dú)占Demo的實(shí)例化挠羔,沒(méi)門!埋嵌!臥槽破加,靜態(tài)被線程A那個(gè)SD上鎖了,哎等等吧雹嗦!
- 線程B:咦范舀!鎖的鑰匙又回來(lái)了,哎了罪,沒(méi)希望了锭环,希望能給我喝點(diǎn)湯。n秒后泊藕,B處于崩潰中辅辩,沒(méi)想到竟然還有if(h == null)這個(gè)大門,我進(jìn)不去了娃圆,嗚嗚嗚玫锋。
值得注意的是,該種產(chǎn)生單例的方式也會(huì)有線程安全的問(wèn)題讼呢,學(xué)過(guò)java的都知道撩鹿,java中在new對(duì)象的時(shí)候,并不是原子操作悦屏,它有以下三個(gè)大概步驟
- 分配內(nèi)存空間
- 初始化對(duì)象
- 將內(nèi)存地址賦給引用h
由于重排序原因(關(guān)于重排序的知識(shí)节沦,可自行網(wǎng)上搜索),可能在new對(duì)象時(shí)窜管,第二步和第三步發(fā)生了交換散劫,導(dǎo)致錯(cuò)誤發(fā)生,理由是幕帆,此時(shí)的h是一個(gè)地址获搏,但它
還沒(méi)完成初始化,如下例子
- 線程A:嘿嘿失乾!我已經(jīng)走到了2常熙,咦!竟然還有鎖碱茁,更好了裸卫,這初始化的好處我就要獨(dú)占了,想想都雞凍纽竣,我要去初始化Demo類的實(shí)例了墓贿,啦啦啦茧泪。
- 線程B:哈哈!線程A那個(gè)SD聋袋,我都走到了1队伟,它竟然還沒(méi)發(fā)現(xiàn)我,還想獨(dú)占Demo的實(shí)例化幽勒,沒(méi)門J任辍!臥槽啥容,靜態(tài)被線程A那個(gè)SD上鎖了锈颗,哎等等吧!
- 線程B:咦咪惠!鎖的鑰匙又回來(lái)了击吱,哎,沒(méi)希望了遥昧,希望能給我喝點(diǎn)湯姨拥。n秒后,B處于崩潰中渠鸽,沒(méi)想到竟然還有if(h == null)這個(gè)大門。
- 線程B:只能認(rèn)命了柴罐,我只能訪問(wèn)Demo對(duì)象玩玩徽缚,噗噗噗,竟然出錯(cuò)了革屠。凿试。
其原因就如上述所說(shuō),發(fā)生了重排序?qū)е碌腻e(cuò)誤發(fā)生似芝,當(dāng)然那婉,這個(gè)錯(cuò)誤不一定經(jīng)常發(fā)生。所有這時(shí)應(yīng)該想的是怎么防止重排序党瓮。
于是有人就想到了java中的volatile關(guān)鍵字來(lái)禁止代碼的重排序详炬,當(dāng)然它還有保證可見(jiàn)性的功能,但不能和synchronized一樣還能保證原子性寞奸。
加了volatile關(guān)鍵字后呛谜,這樣才算真的線程安全,具體代碼如下
public class Demo {
private volatile static Demo h = null;
private Demo(){
}
public static Demo getInstance(){
if(h == null){
synchronized (new Object()) {
h = new Demo();
}
}
return h;
}
}
4颂砸、靜態(tài)內(nèi)部類
靜態(tài)內(nèi)部類就是在該類的內(nèi)部實(shí)現(xiàn)一個(gè)靜態(tài)內(nèi)部類蠢壹,內(nèi)部類里來(lái)實(shí)現(xiàn)該類的實(shí)例化牵舱,具體代碼如下
public class Demo {
private volatile static Demo h = null;
private Demo(){
}
public static Demo getInstance(){
return InnerDemo.h;
}
//內(nèi)部類
private static class InnerDemo{
private final static Demo h = new Demo();
}
}
內(nèi)部類的原理是利用了類加載器classloader機(jī)制來(lái)保證初始化h時(shí)只有一個(gè)線程,這樣也就保證了線程的安全性 聚凹。同時(shí)也不像餓汗式一樣割坠,一進(jìn)入該類就觸發(fā)實(shí)例初始化。內(nèi)部類雖然是static妒牙,但只有在return InnerDemo.h時(shí)才會(huì)觸發(fā)該內(nèi)部類的加載彼哼,也是懶加載的一種。
讀者福利:
分享免費(fèi)學(xué)習(xí)資料
針對(duì)于Java程序員单旁,我這邊準(zhǔn)備免費(fèi)的Java架構(gòu)學(xué)習(xí)資料(里面有高可用沪羔、高并發(fā)、高性能及分布式象浑、Jvm性能調(diào)優(yōu)蔫饰、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個(gè)知識(shí)點(diǎn)的架構(gòu)資料)
為什么某些人會(huì)一直比你優(yōu)秀愉豺,是因?yàn)樗旧砭秃軆?yōu)秀還一直在持續(xù)努力變得更優(yōu)秀篓吁,而你是不是還在滿足于現(xiàn)狀內(nèi)心在竊喜!希望讀到這的您能點(diǎn)個(gè)小贊和關(guān)注下我蚪拦,以后還會(huì)更新技術(shù)干貨杖剪,謝謝您的支持!
資料領(lǐng)取方式:加入Java技術(shù)交流群963944895
驰贷,私信管理員即可免費(fèi)領(lǐng)取