應(yīng)用最廣的模式——單例模式
一、單例模式的定義和關(guān)鍵點(diǎn)
-
定義
確保一個(gè)類只有一個(gè)實(shí)例對象,而且自行實(shí)例化向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。 -
實(shí)現(xiàn)關(guān)鍵點(diǎn)
私有的構(gòu)造器,不能對外開放;
通過靜態(tài)方法或者枚舉返回一個(gè)單例類對象;
確保單例類對象有且僅有一個(gè),尤其是在多線程環(huán)境下;
確保單例類對象在反序列化是不會(huì)重新構(gòu)建對象;
二、UML類圖和應(yīng)用場景
-
應(yīng)用場景
許多時(shí)候整個(gè)系統(tǒng)只需要一個(gè)全局對象,方便我們協(xié)調(diào)整個(gè)系統(tǒng)的行為。比如我們經(jīng)用用于IO、數(shù)據(jù)庫、Log管理等工具類,都可以考慮使用單例模式,這樣避免產(chǎn)生多個(gè)對象耗費(fèi)系統(tǒng)資源。
圖2-1
三、單例模式的幾種寫法
① 餓漢單例
public class Hungry{
//餓漢單例
private static Hungry instance = new Hungry();
private Hungry() {
}
public static synchronized Hungry getInstance() {
return instance;
}
}
?注:該方式每次調(diào)用getInstance()都需要進(jìn)行同步,造成不必要的開銷,所以不推薦使用
② 懶漢單例
public class Idler{
//懶漢單例
private static Idler instace;
private Idler() {
}
public static synchronized Idler getInstance() {
if(instace == null) {
instace = new Idler();
}
return instace;
}
}
?注:情況跟餓漢單例差不多,同樣不推薦使用。
③ DCL單例(雙重檢查)
public class DCL{
//Jdk5之后加入volatitle保證每次都從主內(nèi)存取出
private static volatile DCL sInstance = null;
private DCL() {
}
public static DCL getInstance() {
if(sInstance == null ) {
//既要檢查是否為空對象
//又要檢查是否線程安全
synchronized(DCL.class) {
if(sInstance == null ) {
sInstance = new DCL();
}
}
}
return sInstance;
}
}
?注:該方式在JDK5之前存在DCL失效問題,怎么失效呢?一般我們認(rèn)為編譯器會(huì)按以下順序編譯??
- 給DCL的實(shí)例分配內(nèi)存
- 調(diào)用DCL的構(gòu)造器,初始化成員變量
- 將sInstance引用指向分配的內(nèi)存空間(此時(shí)sInstance不為null)
也就是說2和3 的無法保證順序執(zhí)行,它可能會(huì)執(zhí)行 1->3->2, 這樣的話 線程A已經(jīng)執(zhí)行過了3,但未執(zhí)行2,就切換到線程B,再使用sInstance 就會(huì)出錯(cuò)。
總體來說 JDK5之后,SUN修復(fù)這個(gè)問題,只需將sInstance加入volatile就可以保證每次從主內(nèi)存讀取,這樣或多或少會(huì)影響到一些性能,所以該方式在高并發(fā)的環(huán)境下(高于JDK5版本)一般能滿足需求。
④ 靜態(tài)內(nèi)部單例
public class StaticInner{
//靜態(tài)內(nèi)部類單例
private StaticInner() {
}
private static class StaticInnerHolder{
private static final StaticInner inner = new StaticInner();
}
public static StaticInner getInstance() {
return StaticInnerHolder.inner;
}
}
?注:該方式只有在第一次調(diào)用getInstance()方法才會(huì)使虛擬機(jī)加載StaticInnerHolder類,這樣不僅能保證線程安全,還能保證單例對象的唯一性, 所以推薦使用 。
⑤ 枚舉單例
//枚舉單例
public enum SingletonEum{
INSTANCE;
public void doSomething() {
//各種方法實(shí)現(xiàn)
}
}
?注:這種方式的特點(diǎn)就是簡單,重要的是默認(rèn)枚舉類型的創(chuàng)建是線程安全的,所以《Effective Java》的作者認(rèn)為“單元素的枚舉類型已經(jīng)成為單例模式的最佳方法”,但是枚舉類型在Android的使用也是比較占用內(nèi)存的(一次面試官提到)還需要謹(jǐn)慎使用,我是一臉懵逼的 ??
[2019-8-17]關(guān)于枚舉占用Android 內(nèi)存問題討論:https://www.liaohuqiu.net/cn/posts/android-enum-memory-usage/
以上幾種實(shí)現(xiàn)方式,為了防止反序化時(shí)重新創(chuàng)建對象的情況,還需進(jìn)一步處理,加入readResolve函數(shù)。例如:
public class Hungry implements Serializable{
//餓漢單例
.........
private Object readResolve() throw ObjectStreamException{
return instance;
}
}
四、總結(jié)
| 實(shí)現(xiàn)方式 | 優(yōu)缺點(diǎn) | 說點(diǎn)什么 |
|---|---|---|
| 餓漢&懶漢單例 | 只有使用的才會(huì)被實(shí)例化,但是需要同步開銷 | 不推薦 |
| DCL單例 | 一定程度上解決資源消耗、多余同步線程安全問題,但是因?yàn)镴DK5之前的失效問題需要volatile的加入,影響些性能; | 看著辦吧 |
| 靜態(tài)內(nèi)部類單例 | 能保證線程安全,又能保證單例對象唯一性 | 推薦使用 |
| 枚舉單例 | 簡單方便、線程安全,但Android枚舉類型占用內(nèi)存的憂慮 | 考慮一下 |
??僅學(xué)習(xí)記錄,勉勵(lì)成長;
參考文獻(xiàn):
[1]何紅輝·關(guān)愛民.Android設(shè)計(jì)模式與實(shí)站[J].第二版,2017:25-29.
[2]Joshua Bloch·楊春花.俞黎敏.Effective Java中文版第2版[J].第二版,2017:15.
