單例模式(Singleton)相信大家或多或少都用過,代碼量不多饲做,看起來也很簡單线婚,但是里面的學(xué)問卻不簡單。面試中經(jīng)常會問到單例模式相關(guān)的問題盆均,但是如果你只停留在最簡單的使用上塞弊,那么想讓面試官滿意就很難了。
單例模式用來保證應(yīng)用中有且僅有一個實例泪姨,也是Android中用的較多的一種設(shè)計模式游沿。單例模式有很多使用場景,例如肮砾,當(dāng)一個對象需要頻繁創(chuàng)建诀黍、銷毀時,為了減少內(nèi)存開銷仗处,可以使用單例模式蔗草。
單例模式最關(guān)鍵的兩點:
**1咒彤、私有構(gòu)造方法
2、公有的對外調(diào)用接口
**
單例模式分為兩種咒精,餓漢式和懶漢式镶柱。常見簡單寫法如下所示:
一、餓漢式
public class Singleton{
private static final Singleton instance = new Singleton(); //聲明為static和final模叙,第一次加載類到內(nèi)存中時就會初始化
private Singleton(){}
public static Singleton newInstance(){
return instance;
}
}
二歇拆、懶漢式(線程不安全)
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static Singleton newInstance(){
if(instance == null){
instance = new Singleton(); //方法被調(diào)用的時候才生成實例
}
return instance;
}
}
三、餓漢式與懶漢式的區(qū)別
對比這兩種方式的代碼我們可以看出范咨,餓漢式在類第一次加載進內(nèi)存就實例化了故觅,而懶漢式使用了懶加載模式,當(dāng)方法被調(diào)用時類對象的實例時才生成渠啊。根據(jù)名稱也很好理解输吏,餓漢式很餓,剛一加載進內(nèi)存替蛉,不等你說贯溅,就迫不及待生成“食物”(類的實例)充饑;懶漢式很懶躲查,需要的時候你催我(調(diào)用獲取實例函數(shù))它浅,我才給你生成。
四镣煮、餓漢式與懶漢式的適用場景
餓漢式是最簡單的實現(xiàn)方式姐霍,適合那些在初始化時就要用到單例的情況,如果單例對象初始化非车浯剑快镊折,而且占用內(nèi)存非常小的時候這種方式是比較合適的,可以直接在應(yīng)用啟動時加載并初始化介衔。餓漢式的創(chuàng)建方式在一些場景中無法使用:譬如 Singleton 實例的創(chuàng)建是依賴參數(shù)或者配置文件的恨胚,在 getInstance() 之前必須調(diào)用某個方法設(shè)置參數(shù)給它,那么這種單例寫法就無法使用了夜牡。
懶漢式將單例的初始化操作延遲到需要的時候才進行与纽,若某個單例用的次數(shù)不是很多,但是這個單例提供的功能又非常復(fù)雜塘装,而且加載和初始化要消耗大量的資源急迂,這個時候使用懶漢式就比使用餓漢式合適的多。
五蹦肴、線程安全的懶漢式
不知道大家發(fā)現(xiàn)沒有僚碎,在上面的代碼中關(guān)于懶漢式的寫法存在一個致命的缺點,那就是在多線程的情況下無法正常使用阴幌,當(dāng)多個線程同時調(diào)用getInstance()方法時勺阐,就會創(chuàng)建多個Singleton實例卷中,因此這種寫法是線程不安全的。而餓漢式只會在Singleton類加載進內(nèi)存時實例化一次渊抽,不會出現(xiàn)多次實例化的問題蟆豫,也就不存在線程安全問題。
1懒闷、同步鎖
為了解決懶漢式的線程安全問題十减,最簡單的方法是使用同步鎖synchronized。
public class Singleton {
private static Singleton instance = null;
private Singleton(){ }
public static Singleton getInstance() {
synchronized (Singleton.class) {//防止多線程同時進入造成instance被多次實例化
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
2愤估、雙重檢驗鎖(Double-Check)
這樣做雖然解決了線程安全問題帮辟,但是并不高效,因為在任何時候只能有一個線程調(diào)用 getInstance() 方法玩焰。我們的同步并不是要防止多個線程同時調(diào)用getInstance()由驹,而是防止多個線程同時實例化instatnce。因此昔园,可以在實例化instatnce的地方進行同步蔓榄,如下所示:
public class Singleton {
private static Singleton instance = null;
private Singleton(){ }
public static Singleton getInstance() {
if (instance == null) { //-------------------Single Checked
// 若實例創(chuàng)建了,則不需要同步了蒿赢,直接返回instance即可
synchronized (Singleton.class) { //未創(chuàng)建實例润樱,加鎖
if (instance == null) { //----------------------Double Checked
//若被同步的線程中有一個線程創(chuàng)建了實例渣触,那么別的線程就不用創(chuàng)建了
instance = new Singleton();
}
}
}
return instance;
}
}
其中第二次Check是因為可能會有多個線程一起進入同步塊外的 if羡棵,如果在同步塊內(nèi)不進行二次檢驗的話就會生成多個實例了。
3嗅钻、懶漢式的最佳寫法
上面的Double-check看似完美了皂冰,但是卻存在問題,因為 instance = new Singleton();這句代碼并不是原子操作养篓。在JVM中這句代碼大概做了下面 3 件事情:
(1)給 instance 分配內(nèi)存
(2)調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量秃流,形成實例
(3)將instance對象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 才為非 null)
但是在 JVM 的即時編譯器中存在指令重排序的優(yōu)化。也就是說上面的第(2)步和第(3)步的順序是不能保證的柳弄,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2舶胀。如果是后者,則在 3 執(zhí)行完畢碧注、2 未執(zhí)行之前嚣伐,被其他線程搶占了,這時 instance 已經(jīng)是非 null 了(但卻沒有初始化)萍丐,所以其他線程會直接返回 instance轩端,然后使用這個未初始化的instance,就會順理成章地報錯逝变。
解決辦法也很簡單基茵,只需要將 instance 變量聲明成 volatile 就可以了奋构。我們一般使用volatile關(guān)鍵字有兩個功能:
(1)可見性:volatile關(guān)鍵字修飾的變量不會在多個線程中存在副本,每次都是直接從內(nèi)存中讀取拱层。
(2)禁止指令重排序優(yōu)化弥臼。在 volatile 變量的賦值操作后面會有一個內(nèi)存屏障(生成的匯編代碼上),讀操作不會被重排序到內(nèi)存屏障之前根灯。比如上面的例子醋火,取操作必須在執(zhí)行完 1-2-3 之后或者 1-3-2 之后,不存在執(zhí)行到 1-3 然后取到值的情況箱吕。
public class Singleton {
private volatile static Singleton instance = null; //聲明成 volatile
private Singleton(){ }
public static Singleton getInstance() {
if (instance == null) { //-------------------Single Checked
// 若實例創(chuàng)建了芥驳,則不需要同步了,直接返回instance即可
synchronized (Singleton.class) { //未創(chuàng)建實例茬高,加鎖
if (instance == null) { //----------------------Double Checked
//若被同步的線程中有一個線程創(chuàng)建了實例兆旬,那么別的線程就不用創(chuàng)建了
instance = new Singleton();
}
}
}
return instance;
}
}
六、靜態(tài)內(nèi)部類
public class Singleton {
private static class SingletonHolder {//只有加載內(nèi)部類的時候才初始化
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() { //只有在getInstance()被調(diào)用的時候才被真正創(chuàng)建
return SingletonHolder.INSTANCE;
}
}
這種寫法仍然使用JVM本身機制保證了線程安全問題怎栽。
由于 SingletonHolder 是私有的丽猬,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的熏瞄;同時讀取實例的時候不會進行同步脚祟,沒有性能缺陷;也不依賴 JDK 版本强饮。這種寫法即解決了餓漢式不能延遲加載的缺陷由桌,又解決了懶漢式線程安全的問題,也是《Effective Java》上所推薦的寫法邮丰。
七行您、枚舉(Enum)
public enum Singleton{
INSTANCE; //定義一個枚舉的元素,它就是Singleton的一個實例
}
我們可以通過Singleton.INSTANCE來訪問實例剪廉,這比調(diào)用getInstance()方法簡單多了娃循。默認(rèn)枚舉實例的創(chuàng)建是線程安全的(創(chuàng)建枚舉類的單例在JVM層面也是能保證線程安全的),所以不需要擔(dān)心線程安全的問題斗蒋,而且還能防止反序列化導(dǎo)致重新創(chuàng)建新的對象捌斧。所以理論上枚舉類來實現(xiàn)單例模式是最簡單的方式。但由于大多數(shù)人不太熟悉泉沾,用這種方法寫的人不多捞蚂。
八、總結(jié)
一般來說爆哑,單例模式有五種寫法:餓漢洞难、懶漢、雙重檢驗鎖、靜態(tài)內(nèi)部類队贱、枚舉色冀。上述所說都是線程安全的實現(xiàn),文章開頭給出的第一種懶漢式方法不算正確的寫法柱嫌。
在我們?nèi)粘嵺`中锋恬,一般情況下直接使用餓漢式就好了,如果明確要求要懶加載编丘,則推薦使用靜態(tài)內(nèi)部類与学,如果涉及到反序列化創(chuàng)建對象時,可以使用枚舉的方式來實現(xiàn)單例嘉抓。但是在很多的面試中索守,線程安全的懶漢式是最常考察的抑片,我們需要好好掌握Double-Check的寫法卵佛,注意關(guān)鍵字volatile的使用。
參考文章:
1敞斋、http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
2截汪、http://stormzhang.com/designpattern/2016/03/27/android-design-pattern-singleton/