
概述:
5分鐘理解設(shè)計(jì)模式系列,將通過(guò)解決實(shí)際問(wèn)題,來(lái)帶您理解設(shè)計(jì)模式,本文希望帶您搞懂的3個(gè)問(wèn)題是:
- 為什么使用單例模式?
2.你有哪些實(shí)現(xiàn)單例模式的方法?
3.單例模式是[金手指]嗎?
1 為什么使用單例模式?
單例模式(Singleton Design Pattern),一個(gè)類只允許創(chuàng)建一個(gè)對(duì)象(或者實(shí)例),這個(gè)類就是一個(gè)單例類,這種設(shè)計(jì)模式,就叫做單例模式。
單例模式主要用于:處理資源訪問(wèn)沖突與表示全局唯一。
處理資源訪問(wèn)
例如:我們有一個(gè)Logger類,通過(guò)FileWriter類來(lái)記錄日志,記錄日志的文件位置是:info.log,我們?cè)诔绦蛑锌赡軙?huì)有很多地方需要記錄日志,那么如果我們每一次記錄日志,都創(chuàng)建一個(gè)Logger類的話,那么每一個(gè)Logger類都會(huì)去寫(xiě)info.log文件(每個(gè)Logger對(duì)應(yīng)一個(gè)FileWriter,多個(gè)FileWriter同時(shí)寫(xiě)入),此時(shí)info.log就是一個(gè)競(jìng)爭(zhēng)資源,兩個(gè)線程同時(shí)寫(xiě)入數(shù)據(jù),就可能出現(xiàn)數(shù)據(jù)覆蓋的情況。但是如果Logger類是一個(gè)單例類,那么由于FileWriter是一個(gè)線程安全的對(duì)象,那么就不會(huì)出現(xiàn)數(shù)據(jù)覆蓋的問(wèn)題。
表示全局唯一
在我們的程序開(kāi)發(fā)過(guò)程中,有一些類需要被表示成全局唯一,比如我們的配置文件在系統(tǒng)中只有一份,那么當(dāng)配置文件被加載到內(nèi)存后,以對(duì)象的形式存在,也應(yīng)該存一份,再比如全局的id自增生成器、線程池、對(duì)象池等對(duì)象,也可以設(shè)計(jì)成單例。
2 你有哪些實(shí)現(xiàn)單例模式的方法?
1 餓漢式:
public class Singleton {
private Singleton() {
}
private static Singleton singleton = new Singleton();
public Singleton getInstance(){
return singleton;
}
}
在類加載的時(shí)候,創(chuàng)建對(duì)象,有人覺(jué)得這種實(shí)現(xiàn)方式不好,因?yàn)椴恢С盅舆t加載。
但是我并不認(rèn)同這種說(shuō)法,因?yàn)槿绻搶?duì)象加載耗時(shí)非常長(zhǎng),那么最好不要等到真正去使用它的時(shí)候,再去初始化,因?yàn)檫@樣會(huì)影響性能,甚至?xí)捎诮涌陧憫?yīng)時(shí)間過(guò)長(zhǎng)導(dǎo)致超時(shí)失敗。如果該實(shí)力占用資源較多,可能會(huì)引起程序報(bào)錯(cuò)(比如 Java 中的 PermGen Space OOM),那么在程序初始化時(shí)拋出異常,我們還可以進(jìn)行調(diào)整,但是如果這個(gè)異常是在程序運(yùn)行時(shí)拋出的,那么會(huì)導(dǎo)致整個(gè)程序崩潰,所以有問(wèn)題應(yīng)該提早暴露,遵循fail-fast的設(shè)計(jì)原則
2 雙重檢測(cè)懶漢式
public class Singleton {
private Singleton() {
}
private static Singleton singleton = null;
public Singleton getInstance(){
// 提高性能,降低線程進(jìn)入臨界區(qū)的可能
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
這也是單例模式比較經(jīng)典的寫(xiě)法,網(wǎng)上有人說(shuō)由于指令重排,需要在加volatile關(guān)鍵字,禁止指令重排。實(shí)際上這個(gè)問(wèn)題在高版本的jdk中已經(jīng)修復(fù)了,修復(fù)方法是將對(duì)象的new操作改成原子操作。低版本的指令重排問(wèn)題產(chǎn)生的原因我也寫(xiě)在了文章的最后,感興趣的同學(xué)也可以了解一下。
3 靜態(tài)內(nèi)部類
這是一種比雙重檢測(cè)的單例模式更簡(jiǎn)單的寫(xiě)法,也可以做到延遲加載:
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return Inner.singleton;
}
private static class Inner {
private static Singleton singleton = new Singleton();
}
}
該方法使用的是靜態(tài)內(nèi)部類的特性,外部類被加載的時(shí)候,不會(huì)創(chuàng)建Inner實(shí)例對(duì)象,只有調(diào)用 getInstance方法的時(shí)候,才會(huì)創(chuàng)建Sigleton對(duì)象,創(chuàng)過(guò)程中的線程安全,Singleton的唯一性均由JVM保證
4 枚舉
枚舉是一種最簡(jiǎn)單的實(shí)現(xiàn)方式,通過(guò)java枚舉類的特性,保證唯一性。
public enum Singleton {
INSTANCE
//doSomething 該實(shí)例支持的行為
//可以省略此方法,通過(guò)Singleton.INSTANCE進(jìn)行操作
public static Singleton get Instance() {
return Singleton.INSTANCE;
}
}
3 單例模式是[金手指]嗎?
并不是,單例模式也存在著一些問(wèn)題
對(duì)OOP的特性不友好(不基于接口、對(duì)繼承、多態(tài)并不友好)
不支持有參數(shù)的構(gòu)造函數(shù)
對(duì)程序的可測(cè)試性不友好
對(duì)程序的擴(kuò)展性不友好
如果單例模式的成員變量是全局變量,一但被修改將影響其他調(diào)用類
所以我們可以用過(guò)工廠模式、或者是ioc容器來(lái)代替單例模式,當(dāng)然使用那種對(duì)象創(chuàng)建方法,還應(yīng)該根據(jù)具體的業(yè)務(wù)而定。
選讀:低版本的jdk為什么需要使用到volatile關(guān)鍵字修飾?
public class Singleton {
private Singleton() {
}
private volatile Singleton singleton = null;
public Singleton getInstance(){
// 提高性能,降低線程進(jìn)入臨界區(qū)的可能
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
代碼中有兩處判空:
第一處判空,是為了提高性能,降低線程進(jìn)入臨界區(qū)的可能性。
第二處判空是為了線程同步,假如沒(méi)有第二處判空,則可能兩個(gè)線程都通過(guò)了if(singleton==null)條件,這樣即使是臨界區(qū)內(nèi)只有一個(gè)線程在執(zhí)行,臨界區(qū)內(nèi)的代碼也會(huì)被執(zhí)行兩遍,這樣就會(huì)產(chǎn)生兩個(gè)對(duì)象,不符合單例模式。
成員變量使用了volatile進(jìn)行修飾,一方面是保證了對(duì)象在多線程環(huán)境下的可見(jiàn)性,另一方面是為了防止new Singleton()進(jìn)行指令重排序而導(dǎo)致的并發(fā)問(wèn)題。
volatile關(guān)鍵字的作用兩個(gè):
1 保證變量在線程之間的可見(jiàn)性(直接從主存中讀寫(xiě)數(shù)據(jù),不經(jīng)過(guò)工作內(nèi)存)
2 阻止編譯時(shí)和運(yùn)行時(shí)的指令重排,編譯時(shí)JVM編譯器遵循內(nèi)存屏障約束,運(yùn)行時(shí)依賴CPU屏障來(lái)阻止指令重排。
指令重排是指JVM在編譯Java代碼的時(shí)候,或者CPU在執(zhí)行JVM字節(jié)碼的時(shí)候,對(duì)現(xiàn)有的指令順序進(jìn)行重新排序。
指令重排的目的是為了在不改變程序執(zhí)行結(jié)果的前提下,優(yōu)化程序的運(yùn)行效率。需要注意的是,這里所說(shuō)的不改變執(zhí)行結(jié)果,指的是不改變單線程下的程序執(zhí)行結(jié)果。
這里不太好懂,舉一個(gè)例子,正常的new Singleton()創(chuàng)建步驟是:
1 開(kāi)辟一塊內(nèi)存空間
2 創(chuàng)建對(duì)象
3 將對(duì)象的地址存入引用變量
經(jīng)過(guò)指令重排后,可能變成了:
1 開(kāi)辟一塊內(nèi)存空間
2 將對(duì)象的地址存入引用變量
3 創(chuàng)建對(duì)象
假設(shè)發(fā)生了指令重排,線程A、B都執(zhí)行這段代碼,線程A執(zhí)行到了new Singleton()的步驟2,此時(shí)還沒(méi)有創(chuàng)建對(duì)象,這個(gè)時(shí)候發(fā)生了線程的切換。線程B開(kāi)始執(zhí)行,這個(gè)時(shí)候線程B還可以通過(guò)if(singleton == null)的判斷,因?yàn)榫€程A中的singleton只是指向了一個(gè)空的內(nèi)存地址,這個(gè)時(shí)候線程B創(chuàng)建出了一個(gè)Singleton對(duì)象,當(dāng)線程切換成A時(shí),線程A仍執(zhí)行了new Singleton()的步驟3,此時(shí)創(chuàng)建了2個(gè)Singleton對(duì)象,不符合單例模式。
最后,期待您的訂閱和點(diǎn)贊,專欄每周都會(huì)更新,希望可以和您一起進(jìn)步,同時(shí)也期待您的批評(píng)與指正!