引子
單例模式的文章可以說是百家爭鳴,今天我也來說道說道狰晚,大家共同提升收厨。
單例模式的作用和使用場景
單例模式(Singleton Pattern)
確保某一個類只有一個實例,而且可以自行實例化并向整個系統(tǒng)提供這個實例妒蛇,這個類稱為單例類机断,它提供全局訪問的方法。 單例模式是一種對象創(chuàng)建型模式绣夺。
使用場景
比如一個應用中應該只存在一個ImageLoader實例吏奸。
Android中的LayoutInflater類等。
EventBus中getDefault()方法獲取實例陶耍。
保證對象唯一
1. 為了避免其他程序過多建立該類對象奋蔚。先禁止其他程序建立該類對象
2. 還為了讓其他程序可以訪問到該類對象,只好在本類中烈钞,自定義一個對象泊碑。
3. 為了方便其他程序對自定義對象的訪問,可以對外提供一些訪問方式毯欣。
這三步怎么用代碼體現(xiàn)呢馒过?
1. 將構造函數(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;
}
}
上面三種寫法本質上其實是一樣的嘹锁,也是各類文章在介紹餓漢式時常用的方式。但使用靜態(tài)final的實例對象或者使用靜態(tài)代碼塊依舊不能解決在反序列化着裹、反射领猾、克隆時重新生成實例對象的問題。
序列化:一是可以將一個單例的實例對象寫到磁盤骇扇,實現(xiàn)數(shù)據(jù)的持久化摔竿;二是實現(xiàn)對象數(shù)據(jù)的遠程傳輸。
當單例對象有必要實現(xiàn) Serializable 接口時匠题,即使將其構造函數(shù)設為私有拯坟,在它反序列化時依然會通過特殊的途徑再創(chuàng)建類的一個新的實例,相當于調用了該類的構造函數(shù)有效地獲得了一個新實例韭山!
反射:可以通過setAccessible(true)來繞過 private 限制,從而調用到類的私有構造函數(shù)創(chuàng)建對象冷溃。
克虑酢:clone()是 Object 的方法,每一個對象都是 Object 的子類似枕,都有clone()方法盖淡。clone()方法并不是調用構造函數(shù)來創(chuàng)建對象,而是直接拷貝內存區(qū)域凿歼。因此當我們的單例對象實現(xiàn)了 Cloneable 接口時褪迟,盡管其構造函數(shù)是私有的,仍可以通過克隆來創(chuàng)建一個新對象答憔,單例模式也相應失效了味赃。
優(yōu)點:寫法比較簡單,在類裝載的時候就完成實例化虐拓。避免了線程同步問題心俗。
缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果蓉驹。如果從始至終從未使用過這個實例城榛,則會造成內存的浪費。
那么我們就要考慮懶加載的問題了态兴。
四狠持、懶漢式(線程不安全)
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance== null) {
instance = new Singleton();
}
return instance;
}
}
優(yōu)點:懶加載,只有使用的時候才會加載瞻润。
缺點:但是只能在單線程下使用喘垂。如果在多線程下甜刻,instacnce對象還是空,這時候兩個線程同時訪問getInstance()方法王污,因為對象還是空罢吃,所以兩個線程同時通過了判斷,開始執(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)點:懶加載就谜,只有使用的時候才會加載,獲取單例方法加了同步鎖里覆,保正了線程安全丧荐。
缺點:效率太低了,每個線程在想獲得類的實例時候喧枷,執(zhí)行getInstance()方法都要進行同步虹统。
六、懶漢式(線程假裝安全隧甚,同步代碼塊)
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)點:改進了第五種效率低的問題车荔。
缺點:但實際上這個寫法還不能保證線程安全,和第四種寫法類似戚扳,只要兩個線程同時進入了 if (singleton == null) { 這句判斷忧便,照樣會進行兩次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;
}
}
本例的亮點都在getInstance()方法上珠增,可以看到在該方法中對instance進行了兩次判空:第一層判斷為了避免不必要的同步,第二層判斷則是為了在null的情況下創(chuàng)建實例砍艾。對第六種單例的漏洞進行了彌補蒂教,但是還是有丶小問題的,問題就在instance = new Singleton();語句上辐董。
這語句在這里看起來是一句代碼啊悴品,但實際上它并不是一個原子操作,這句代碼最終會被編譯成多條匯編指令简烘,它大致做了3件事情:
1. 給Singleton的實例分配內存
2. 調用Singleton()的 構造函數(shù)苔严,初始化成員字段
3. 將instance對象指向分配的內存空間(此時instance就不是null了)
但是,由于Java編譯器運行處理器亂序執(zhí)行孤澎,以及jdk1.5之前Java內存模型中Cache届氢、寄存器到主內存會寫順序的規(guī)定,上面的第二和第三的順序是無法保證的覆旭。也就是說退子,執(zhí)行順序可能是1-2-3也可能是1-3-2.如果是后者岖妄,并且在3執(zhí)行完畢、2未執(zhí)行之前寂祥,被切換到線程B上荐虐,這時候instance因為已經在線程A內執(zhí)行3了,instance已經是非null丸凭,所有線程B直接取走instance福扬,再使用時就會出錯,這就是DCL失效問題惜犀,而且這種難以跟蹤難以重現(xiàn)的問題很可能會隱藏很久铛碑。
優(yōu)點:線程安全;延遲加載虽界;效率較高汽烦。
缺點:JVM編譯器的指令重排導致單例出現(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之后,官方已經注意到這種問題礁叔,調整了JVM梢夯、具體化了volatile關鍵字,因此晴圾,如果是1.5或之后的版本,只需要將instance的定義改成private static volatile Singleton instance = null;
就可以保證instance對象每次都是從主內存中讀取噪奄,就可以使用DCL的寫法來完成單例模式死姚。當然,volatile多少會影響到性能勤篮,但考慮到程序的正確性都毒,犧牲這點性能還是值得的。
優(yōu)點:線程安全碰缔;延遲加載账劲;效率較高。
缺點:由于volatile關鍵字會屏蔽Java虛擬機所做的一些代碼優(yōu)化金抡,略微的性能降低瀑焦,但除非你的代碼在并發(fā)場景比較復雜或者低于JDK6版本下使用,否則梗肝,這種方式一般是能夠滿足需求的榛瓮。
九、靜態(tài)內部類(推薦使用)
public class Singleton {
private Singleton() {
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
這種方式跟餓漢式方式采用的機制類似巫击,但又有不同禀晓。
兩者都是采用了類裝載的機制來保證初始化實例時只有一個線程精续。不同的地方在餓漢式方式是只要Singleton類被裝載就會實例化,沒有Lazy-Loading的作用粹懒,而靜態(tài)內部類方式在Singleton類被裝載時并不會立即實例化重付,而是在需要實例化時,調用getInstance方法凫乖,才會裝載SingletonInstance類确垫,從而完成Singleton的實例化。
所以在這里拣凹,利用 JVM的 classloder 的機制來保證初始化 instance 時只有一個線程森爽。JVM 在類初始化階段會獲取一個鎖,這個鎖可以同步多個線程對同一個類的初始化
優(yōu)點:避免了線程不安全嚣镜,延遲加載爬迟,效率高。
缺點:依舊不能解決在反序列化菊匿、反射付呕、克隆時重新生成實例對象的問題。
十跌捆、枚舉
public enum Singleton {
INSTANCE
}
枚舉類單例模式是《Effective Java》作者 Josh Bloch 極力推薦的單例方法
借助JDK 1.5中添加的枚舉來實現(xiàn)單例模式佩厚。P.S. Enum是沒有clone()方法的
1. 枚舉類類型是 final 的「不可以被繼承」
2. 構造方法是私有的「也只能私有潮瓶,不允許被外部實例化毯辅,符合單例」
3. 類變量是靜態(tài)的
4. 沒有延時初始化,隨著類的初始化就初始化了「從上面靜態(tài)代碼塊中可以看出」
5. 由 4 可以知道枚舉也是線程安全的
優(yōu)點:寫法簡單,不僅能避免多線程同步問題嗜逻,而且還能防止反序列化栈顷、反射室抽、克隆重新創(chuàng)建新的對象坪圾。
缺點: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)點:在程序的初始,將多種單例類型注入到一個統(tǒng)一的管理類中,在使用時根據(jù)key獲取對象對應類型的對象隧期。這種方式使得我們可以管理多種類型的單例,并且在使用時可以通過統(tǒng)一的接口進行獲取操作檐晕, 降低了用戶的使用成本辟灰,也對用戶隱藏了具體實現(xiàn)西采,降低了耦合度胖眷。
缺點:不常用珊搀,有些麻煩
十二、內部枚舉類
在微信公眾號看到有大佬說使用枚舉配合內部類實現(xiàn)內部枚舉類,可以達成線程安全沛鸵,懶加載,責任單一原則,等等是現(xiàn)在最完美的寫法。
總結
如果你和我一樣是Android開發(fā),那么由于在客戶端通常沒有高并發(fā)的情況,選擇哪種實現(xiàn)方式并不會有太大的影響。但即便如此湿颅,出于效率考慮我們也應該使用后面幾種單例方法。
單例模式的優(yōu)點
單例模式的優(yōu)點其實已經在定義中提現(xiàn)了:可以減少系統(tǒng)內存開支执赡,減少系統(tǒng)性能開銷驻呐,避免對資源的多重占用、同時操作挎袜。
單例模式的缺點
1. 違反了單一責任鏈原則,測試困難
單例類的職責過重肥惭,在一定程度上違背了“單一職責原則”盯仪。因為單例類既充當了工廠角色,提供了工廠方法蜜葱,同時又充當了產品角色全景,包含一些業(yè)務方法,將產品的創(chuàng)建和產品的本身的功能融合到一起牵囤。
2. 擴展困難
由于單例模式中沒有抽象層爸黄,因此單例類的擴展有很大的困難。修改功能必須修改源碼揭鳞。
3. 共享資源有可能不一致炕贵。
現(xiàn)在很多面向對象語言(如Java、C#)的運行環(huán)境都提供了自動垃圾回收的技術野崇,因此称开,如果實例化的共享對象長時間不被利用,系統(tǒng)會認為它是垃圾乓梨,會自動銷毀并回收資源钥弯,下次利用時又將重新實例化,這將導致共享的單例對象狀態(tài)的丟失督禽。
注意在Application中存取數(shù)據(jù)
在Android 應用啟動后、任意組件被創(chuàng)建前总处,系統(tǒng)會自動為應用創(chuàng)建一個 Application類(或其子類)的對象狈惫,且只創(chuàng)建一個。從此它就一直在那里,直到應用的進程被殺掉胧谈。
所以雖然 Application并沒有采用單例模式來實現(xiàn)忆肾,但是由于它的生命周期由框架來控制,和整個應用的保持一致菱肖,且確保了只有一個客冈,所以可以被看作是一個單例。
但是如果你直接用它來存取數(shù)據(jù)稳强,那你將得到無窮無盡的NullPointerException场仲。
因為Application 不會永遠駐留在內存里,隨著進程被殺掉退疫,Application 也被銷毀了渠缕,再次使用時,它會被重新創(chuàng)建褒繁,它之前保存下來的所有狀態(tài)都會被重置亦鳞。
要預防這個問題,我們不能用 Application 對象來傳遞數(shù)據(jù)棒坏,而是要:
1. 通過傳統(tǒng)的 intent 來顯式傳遞數(shù)據(jù)(將 Parcelable 或 Serializable 對象放入Intent / Bundle燕差。Parcelable 性能比 Serializable 快一個量級,但是代碼實現(xiàn)要復雜一些)坝冕。
2. 重寫onSaveInstanceState()以及onRestoreInstanceState()方法徒探,確保進程被殺掉時保存了必須的應用狀態(tài),從而在重新打開時可以正確恢復現(xiàn)場徽诲。
3. 使用合適的方式將數(shù)據(jù)保存到數(shù)據(jù)庫或硬盤刹帕。
4. 總是做判空保護和處理。
參考文章
《Android 源碼設計模式解析與實戰(zhàn)》
https://www.cnblogs.com/zhaoyan001/p/6365064.html