引子
單例模式的文章可以說是百家爭鳴,今天我也來說道說道,大家共同提升。
單例模式的作用和使用場景
單例模式(Singleton Pattern)
確保某一個類只有一個實(shí)例,而且可以自行實(shí)例化并向整個系統(tǒng)提供這個實(shí)例,這個類稱為單例類,它提供全局訪問的方法。 單例模式是一種對象創(chuàng)建型模式。
使用場景
比如一個應(yīng)用中應(yīng)該只存在一個ImageLoader實(shí)例。
Android中的LayoutInflater類等。
EventBus中g(shù)etDefault()方法獲取實(shí)例。
保證對象唯一
1. 為了避免其他程序過多建立該類對象。先禁止其他程序建立該類對象
2. 還為了讓其他程序可以訪問到該類對象,只好在本類中,自定義一個對象。
3. 為了方便其他程序?qū)ψ远x對象的訪問,可以對外提供一些訪問方式。
這三步怎么用代碼體現(xiàn)呢?
1. 將構(gòu)造函數(shù)私有化。
2. 在類中創(chuàng)建一個本類對象。
3. 提供一個方法可以獲取到該對象。
單例模式的十二種寫法
一、餓漢式(靜態(tài)變量)
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
二、餓漢式(靜態(tài)常量)
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
三、餓漢式(靜態(tài)代碼塊)
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
上面三種寫法本質(zhì)上其實(shí)是一樣的,也是各類文章在介紹餓漢式時常用的方式。但使用靜態(tài)final的實(shí)例對象或者使用靜態(tài)代碼塊依舊不能解決在反序列化、反射、克隆時重新生成實(shí)例對象的問題。
序列化:一是可以將一個單例的實(shí)例對象寫到磁盤,實(shí)現(xiàn)數(shù)據(jù)的持久化;二是實(shí)現(xiàn)對象數(shù)據(jù)的遠(yuǎn)程傳輸。
當(dāng)單例對象有必要實(shí)現(xiàn) Serializable 接口時,即使將其構(gòu)造函數(shù)設(shè)為私有,在它反序列化時依然會通過特殊的途徑再創(chuàng)建類的一個新的實(shí)例,相當(dāng)于調(diào)用了該類的構(gòu)造函數(shù)有效地獲得了一個新實(shí)例!
反射:可以通過setAccessible(true)來繞過 private 限制,從而調(diào)用到類的私有構(gòu)造函數(shù)創(chuàng)建對象。
克隆:clone()是 Object 的方法,每一個對象都是 Object 的子類,都有clone()方法。clone()方法并不是調(diào)用構(gòu)造函數(shù)來創(chuàng)建對象,而是直接拷貝內(nèi)存區(qū)域。因此當(dāng)我們的單例對象實(shí)現(xiàn)了 Cloneable 接口時,盡管其構(gòu)造函數(shù)是私有的,仍可以通過克隆來創(chuàng)建一個新對象,單例模式也相應(yīng)失效了。
優(yōu)點(diǎn):寫法比較簡單,在類裝載的時候就完成實(shí)例化。避免了線程同步問題。
缺點(diǎn):在類裝載的時候就完成實(shí)例化,沒有達(dá)到Lazy Loading的效果。如果從始至終從未使用過這個實(shí)例,則會造成內(nèi)存的浪費(fèi)。
那么我們就要考慮懶加載的問題了。
四、懶漢式(線程不安全)
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance== null) {
instance = new Singleton();
}
return instance;
}
}
優(yōu)點(diǎn):懶加載,只有使用的時候才會加載。
缺點(diǎn):但是只能在單線程下使用。如果在多線程下,instacnce對象還是空,這時候兩個線程同時訪問getInstance()方法,因?yàn)閷ο筮€是空,所以兩個線程同時通過了判斷,開始執(zhí)行new的操作。所以在多線程環(huán)境下不可使用這種方式。
五、懶漢式(線程安全,存在同步開銷)
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
優(yōu)點(diǎn):懶加載,只有使用的時候才會加載,獲取單例方法加了同步鎖,保正了線程安全。
缺點(diǎn):效率太低了,每個線程在想獲得類的實(shí)例時候,執(zhí)行g(shù)etInstance()方法都要進(jìn)行同步。
六、懶漢式(線程假裝安全,同步代碼塊)
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
優(yōu)點(diǎn):改進(jìn)了第五種效率低的問題。
缺點(diǎn):但實(shí)際上這個寫法還不能保證線程安全,和第四種寫法類似,只要兩個線程同時進(jìn)入了 if (singleton == null) { 這句判斷,照樣會進(jìn)行兩次new操作
接下來就是聽起來很牛逼的雙重檢測加鎖的單例模式。
七、DCL「雙重檢測鎖:Double Checked Lock」 單例(假)
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
本例的亮點(diǎn)都在getInstance()方法上,可以看到在該方法中對instance進(jìn)行了兩次判空:第一層判斷為了避免不必要的同步,第二層判斷則是為了在null的情況下創(chuàng)建實(shí)例。對第六種單例的漏洞進(jìn)行了彌補(bǔ),但是還是有丶小問題的,問題就在instance = new Singleton();語句上。
這語句在這里看起來是一句代碼啊,但實(shí)際上它并不是一個原子操作,這句代碼最終會被編譯成多條匯編指令,它大致做了3件事情:
1. 給Singleton的實(shí)例分配內(nèi)存
2. 調(diào)用Singleton()的 構(gòu)造函數(shù),初始化成員字段
3. 將instance對象指向分配的內(nèi)存空間(此時instance就不是null了)
但是,由于Java編譯器運(yùn)行處理器亂序執(zhí)行,以及jdk1.5之前Java內(nèi)存模型中Cache、寄存器到主內(nèi)存會寫順序的規(guī)定,上面的第二和第三的順序是無法保證的。也就是說,執(zhí)行順序可能是1-2-3也可能是1-3-2.如果是后者,并且在3執(zhí)行完畢、2未執(zhí)行之前,被切換到線程B上,這時候instance因?yàn)橐呀?jīng)在線程A內(nèi)執(zhí)行3了,instance已經(jīng)是非null,所有線程B直接取走instance,再使用時就會出錯,這就是DCL失效問題,而且這種難以跟蹤難以重現(xiàn)的問題很可能會隱藏很久。
優(yōu)點(diǎn):線程安全;延遲加載;效率較高。
缺點(diǎn):JVM編譯器的指令重排導(dǎo)致單例出現(xiàn)漏洞。
八、DCL「雙重檢測鎖:Double Checked Lock」 單例(真,推薦使用)
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
在jdk1.5之后,官方已經(jīng)注意到這種問題,調(diào)整了JVM、具體化了volatile關(guān)鍵字,因此,如果是1.5或之后的版本,只需要將instance的定義改成private static volatile Singleton instance = null;
就可以保證instance對象每次都是從主內(nèi)存中讀取,就可以使用DCL的寫法來完成單例模式。當(dāng)然,volatile多少會影響到性能,但考慮到程序的正確性,犧牲這點(diǎn)性能還是值得的。
優(yōu)點(diǎn):線程安全;延遲加載;效率較高。
缺點(diǎn):由于volatile關(guān)鍵字會屏蔽Java虛擬機(jī)所做的一些代碼優(yōu)化,略微的性能降低,但除非你的代碼在并發(fā)場景比較復(fù)雜或者低于JDK6版本下使用,否則,這種方式一般是能夠滿足需求的。
九、靜態(tài)內(nèi)部類(推薦使用)
public class Singleton {
private Singleton() {
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
這種方式跟餓漢式方式采用的機(jī)制類似,但又有不同。
兩者都是采用了類裝載的機(jī)制來保證初始化實(shí)例時只有一個線程。不同的地方在餓漢式方式是只要Singleton類被裝載就會實(shí)例化,沒有Lazy-Loading的作用,而靜態(tài)內(nèi)部類方式在Singleton類被裝載時并不會立即實(shí)例化,而是在需要實(shí)例化時,調(diào)用getInstance方法,才會裝載SingletonInstance類,從而完成Singleton的實(shí)例化。
所以在這里,利用 JVM的 classloder 的機(jī)制來保證初始化 instance 時只有一個線程。JVM 在類初始化階段會獲取一個鎖,這個鎖可以同步多個線程對同一個類的初始化
優(yōu)點(diǎn):避免了線程不安全,延遲加載,效率高。
缺點(diǎn):依舊不能解決在反序列化、反射、克隆時重新生成實(shí)例對象的問題。
十、枚舉
public enum Singleton {
INSTANCE
}
枚舉類單例模式是《Effective Java》作者 Josh Bloch 極力推薦的單例方法
借助JDK 1.5中添加的枚舉來實(shí)現(xiàn)單例模式。P.S. Enum是沒有clone()方法的
1. 枚舉類類型是 final 的「不可以被繼承」
2. 構(gòu)造方法是私有的「也只能私有,不允許被外部實(shí)例化,符合單例」
3. 類變量是靜態(tài)的
4. 沒有延時初始化,隨著類的初始化就初始化了「從上面靜態(tài)代碼塊中可以看出」
5. 由 4 可以知道枚舉也是線程安全的
優(yōu)點(diǎn):寫法簡單,不僅能避免多線程同步問題,而且還能防止反序列化、反射、克隆重新創(chuàng)建新的對象。
缺點(diǎn):JDK 1.5之后才能使用。
十一、登記式單例--使用Map容器來管理單例模式
public class SingletonManger {
private static Map<String, Object> objectMap = new HashMap<String, Object>();
private SingletonManger() {
}
public static void registerService(String key, Object instance) {
if (!objectMap.containsKey(key)) {
objectMap.put(key, instance);
}
}
public static Object getService(String key) {
return objectMap.get(key);
}
}
查閱Android源碼中的 LayoutInflater 對象就能發(fā)現(xiàn)使用了這種寫法
優(yōu)點(diǎn):在程序的初始,將多種單例類型注入到一個統(tǒng)一的管理類中,在使用時根據(jù)key獲取對象對應(yīng)類型的對象。這種方式使得我們可以管理多種類型的單例,并且在使用時可以通過統(tǒng)一的接口進(jìn)行獲取操作, 降低了用戶的使用成本,也對用戶隱藏了具體實(shí)現(xiàn),降低了耦合度。
缺點(diǎn):不常用,有些麻煩
十二、內(nèi)部枚舉類
在微信公眾號看到有大佬說使用枚舉配合內(nèi)部類實(shí)現(xiàn)內(nèi)部枚舉類,可以達(dá)成線程安全,懶加載,責(zé)任單一原則,等等是現(xiàn)在最完美的寫法。

總結(jié)
如果你和我一樣是Android開發(fā),那么由于在客戶端通常沒有高并發(fā)的情況,選擇哪種實(shí)現(xiàn)方式并不會有太大的影響。但即便如此,出于效率考慮我們也應(yīng)該使用后面幾種單例方法。
單例模式的優(yōu)點(diǎn)
單例模式的優(yōu)點(diǎn)其實(shí)已經(jīng)在定義中提現(xiàn)了:可以減少系統(tǒng)內(nèi)存開支,減少系統(tǒng)性能開銷,避免對資源的多重占用、同時操作。
單例模式的缺點(diǎn)
1. 違反了單一責(zé)任鏈原則,測試?yán)щy
單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”。因?yàn)閱卫惣瘸洚?dāng)了工廠角色,提供了工廠方法,同時又充當(dāng)了產(chǎn)品角色,包含一些業(yè)務(wù)方法,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起。
2. 擴(kuò)展困難
由于單例模式中沒有抽象層,因此單例類的擴(kuò)展有很大的困難。修改功能必須修改源碼。
3. 共享資源有可能不一致。
現(xiàn)在很多面向?qū)ο笳Z言(如Java、C#)的運(yùn)行環(huán)境都提供了自動垃圾回收的技術(shù),因此,如果實(shí)例化的共享對象長時間不被利用,系統(tǒng)會認(rèn)為它是垃圾,會自動銷毀并回收資源,下次利用時又將重新實(shí)例化,這將導(dǎo)致共享的單例對象狀態(tài)的丟失。
注意在Application中存取數(shù)據(jù)
在Android 應(yīng)用啟動后、任意組件被創(chuàng)建前,系統(tǒng)會自動為應(yīng)用創(chuàng)建一個 Application類(或其子類)的對象,且只創(chuàng)建一個。從此它就一直在那里,直到應(yīng)用的進(jìn)程被殺掉。
所以雖然 Application并沒有采用單例模式來實(shí)現(xiàn),但是由于它的生命周期由框架來控制,和整個應(yīng)用的保持一致,且確保了只有一個,所以可以被看作是一個單例。
但是如果你直接用它來存取數(shù)據(jù),那你將得到無窮無盡的NullPointerException。
因?yàn)锳pplication 不會永遠(yuǎn)駐留在內(nèi)存里,隨著進(jìn)程被殺掉,Application 也被銷毀了,再次使用時,它會被重新創(chuàng)建,它之前保存下來的所有狀態(tài)都會被重置。
要預(yù)防這個問題,我們不能用 Application 對象來傳遞數(shù)據(jù),而是要:
1. 通過傳統(tǒng)的 intent 來顯式傳遞數(shù)據(jù)(將 Parcelable 或 Serializable 對象放入Intent / Bundle。Parcelable 性能比 Serializable 快一個量級,但是代碼實(shí)現(xiàn)要復(fù)雜一些)。
2. 重寫onSaveInstanceState()以及onRestoreInstanceState()方法,確保進(jìn)程被殺掉時保存了必須的應(yīng)用狀態(tài),從而在重新打開時可以正確恢復(fù)現(xiàn)場。
3. 使用合適的方式將數(shù)據(jù)保存到數(shù)據(jù)庫或硬盤。
4. 總是做判空保護(hù)和處理。
參考文章
《Android 源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)》
https://www.cnblogs.com/zhaoyan001/p/6365064.html