概述:
5分鐘理解設(shè)計模式系列,將通過解決實際問題,來帶您理解設(shè)計模式乌询,本文希望帶您搞懂的3個問題是:
- 為什么使用單例模式?
2.你有哪些實現(xiàn)單例模式的方法豌研?
3.單例模式是[金手指]嗎妹田?
1 為什么使用單例模式?
單例模式(Singleton Design Pattern)鹃共,一個類只允許創(chuàng)建一個對象(或者實例)鬼佣,這個類就是一個單例類,這種設(shè)計模式霜浴,就叫做單例模式晶衷。
單例模式主要用于:處理資源訪問沖突與表示全局唯一。
處理資源訪問
例如:我們有一個Logger類阴孟,通過FileWriter類來記錄日志晌纫,記錄日志的文件位置是:info.log,我們在程序中可能會有很多地方需要記錄日志永丝,那么如果我們每一次記錄日志锹漱,都創(chuàng)建一個Logger類的話,那么每一個Logger類都會去寫info.log文件(每個Logger對應(yīng)一個FileWriter慕嚷,多個FileWriter同時寫入)哥牍,此時info.log就是一個競爭資源,兩個線程同時寫入數(shù)據(jù)喝检,就可能出現(xiàn)數(shù)據(jù)覆蓋的情況嗅辣。但是如果Logger類是一個單例類,那么由于FileWriter是一個線程安全的對象挠说,那么就不會出現(xiàn)數(shù)據(jù)覆蓋的問題澡谭。
表示全局唯一
在我們的程序開發(fā)過程中钙态,有一些類需要被表示成全局唯一嘉冒,比如我們的配置文件在系統(tǒng)中只有一份,那么當配置文件被加載到內(nèi)存后蔫耽,以對象的形式存在撩炊,也應(yīng)該存一份外永,再比如全局的id自增生成器、線程池拧咳、對象池等對象伯顶,也可以設(shè)計成單例。
2 你有哪些實現(xiàn)單例模式的方法骆膝?
1 餓漢式:
public class Singleton {
private Singleton() {
}
private static Singleton singleton = new Singleton();
public Singleton getInstance(){
return singleton;
}
}
在類加載的時候祭衩,創(chuàng)建對象,有人覺得這種實現(xiàn)方式不好阅签,因為不支持延遲加載掐暮。
但是我并不認同這種說法,因為如果該對象加載耗時非常長政钟,那么最好不要等到真正去使用它的時候路克,再去初始化樟结,因為這樣會影響性能,甚至會由于接口響應(yīng)時間過長導(dǎo)致超時失敗精算。如果該實力占用資源較多瓢宦,可能會引起程序報錯(比如 Java 中的 PermGen Space OOM),那么在程序初始化時拋出異常灰羽,我們還可以進行調(diào)整驮履,但是如果這個異常是在程序運行時拋出的,那么會導(dǎo)致整個程序崩潰廉嚼,所以有問題應(yīng)該提早暴露玫镐,遵循fail-fast的設(shè)計原則
2 雙重檢測懶漢式
public class Singleton {
private Singleton() {
}
private static Singleton singleton = null;
public Singleton getInstance(){
// 提高性能,降低線程進入臨界區(qū)的可能
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
這也是單例模式比較經(jīng)典的寫法怠噪,網(wǎng)上有人說由于指令重排恐似,需要在加volatile關(guān)鍵字,禁止指令重排舰绘。實際上這個問題在高版本的jdk中已經(jīng)修復(fù)了蹂喻,修復(fù)方法是將對象的new操作改成原子操作。低版本的指令重排問題產(chǎn)生的原因我也寫在了文章的最后捂寿,感興趣的同學(xué)也可以了解一下口四。
3 靜態(tài)內(nèi)部類
這是一種比雙重檢測的單例模式更簡單的寫法,也可以做到延遲加載:
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)部類的特性秦陋,外部類被加載的時候蔓彩,不會創(chuàng)建Inner實例對象,只有調(diào)用 getInstance方法的時候驳概,才會創(chuàng)建Sigleton對象赤嚼,創(chuàng)過程中的線程安全,Singleton的唯一性均由JVM保證
4 枚舉
枚舉是一種最簡單的實現(xiàn)方式顺又,通過java枚舉類的特性更卒,保證唯一性。
public enum Singleton {
INSTANCE
//doSomething 該實例支持的行為
//可以省略此方法稚照,通過Singleton.INSTANCE進行操作
public static Singleton get Instance() {
return Singleton.INSTANCE;
}
}
3 單例模式是[金手指]嗎蹂空?
并不是,單例模式也存在著一些問題
對OOP的特性不友好(不基于接口果录、對繼承上枕、多態(tài)并不友好)
不支持有參數(shù)的構(gòu)造函數(shù)
對程序的可測試性不友好
對程序的擴展性不友好
如果單例模式的成員變量是全局變量,一但被修改將影響其他調(diào)用類
所以我們可以用過工廠模式弱恒、或者是ioc容器來代替單例模式辨萍,當然使用那種對象創(chuàng)建方法,還應(yīng)該根據(jù)具體的業(yè)務(wù)而定返弹。
選讀:低版本的jdk為什么需要使用到volatile關(guān)鍵字修飾锈玉?
public class Singleton {
private Singleton() {
}
private volatile Singleton singleton = null;
public Singleton getInstance(){
// 提高性能爪飘,降低線程進入臨界區(qū)的可能
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
代碼中有兩處判空:
第一處判空,是為了提高性能嘲玫,降低線程進入臨界區(qū)的可能性悦施。
第二處判空是為了線程同步,假如沒有第二處判空去团,則可能兩個線程都通過了if(singleton==null)條件,這樣即使是臨界區(qū)內(nèi)只有一個線程在執(zhí)行穷蛹,臨界區(qū)內(nèi)的代碼也會被執(zhí)行兩遍土陪,這樣就會產(chǎn)生兩個對象,不符合單例模式肴熏。
成員變量使用了volatile進行修飾鬼雀,一方面是保證了對象在多線程環(huán)境下的可見性,另一方面是為了防止new Singleton()進行指令重排序而導(dǎo)致的并發(fā)問題蛙吏。
volatile關(guān)鍵字的作用兩個:
1 保證變量在線程之間的可見性(直接從主存中讀寫數(shù)據(jù)源哩,不經(jīng)過工作內(nèi)存)
2 阻止編譯時和運行時的指令重排,編譯時JVM編譯器遵循內(nèi)存屏障約束鸦做,運行時依賴CPU屏障來阻止指令重排励烦。
指令重排是指JVM在編譯Java代碼的時候,或者CPU在執(zhí)行JVM字節(jié)碼的時候泼诱,對現(xiàn)有的指令順序進行重新排序坛掠。
指令重排的目的是為了在不改變程序執(zhí)行結(jié)果的前提下,優(yōu)化程序的運行效率治筒。需要注意的是屉栓,這里所說的不改變執(zhí)行結(jié)果,指的是不改變單線程下的程序執(zhí)行結(jié)果耸袜。
這里不太好懂友多,舉一個例子,正常的new Singleton()創(chuàng)建步驟是:
1 開辟一塊內(nèi)存空間
2 創(chuàng)建對象
3 將對象的地址存入引用變量
經(jīng)過指令重排后堤框,可能變成了:
1 開辟一塊內(nèi)存空間
2 將對象的地址存入引用變量
3 創(chuàng)建對象
假設(shè)發(fā)生了指令重排域滥,線程A、B都執(zhí)行這段代碼胰锌,線程A執(zhí)行到了new Singleton()的步驟2骗绕,此時還沒有創(chuàng)建對象,這個時候發(fā)生了線程的切換资昧。線程B開始執(zhí)行酬土,這個時候線程B還可以通過if(singleton == null)的判斷,因為線程A中的singleton只是指向了一個空的內(nèi)存地址格带,這個時候線程B創(chuàng)建出了一個Singleton對象撤缴,當線程切換成A時刹枉,線程A仍執(zhí)行了new Singleton()的步驟3,此時創(chuàng)建了2個Singleton對象屈呕,不符合單例模式微宝。
最后,期待您的訂閱和點贊虎眨,專欄每周都會更新蟋软,希望可以和您一起進步,同時也期待您的批評與指正嗽桩!