1.簡(jiǎn)述
單例模式是應(yīng)用最廣泛的模式之一,定義就是單例對(duì)象的類(lèi)必須保證只有一個(gè)實(shí)例存在太雨。單例模式適用于創(chuàng)建一個(gè)對(duì)象需要消耗過(guò)多資源的情況吟榴,例如訪問(wèn)數(shù)據(jù)庫(kù)等資源是需要考慮使用。
實(shí)現(xiàn)單例模式的關(guān)鍵點(diǎn)如下:
- 構(gòu)造函數(shù)私有化(才不會(huì)讓你有機(jī)會(huì)再創(chuàng)建一個(gè)對(duì)象)
- 通過(guò)一個(gè)靜態(tài)方法或枚舉(后面會(huì)有舉例)返回單例類(lèi)對(duì)象
- 確保單例類(lèi)的對(duì)象有且只有一個(gè)囊扳,尤其是多線程環(huán)境下(同時(shí)是難點(diǎn))
- 確保單例類(lèi)的對(duì)象在反序列化是不會(huì)重新構(gòu)建對(duì)象
2.實(shí)現(xiàn)
餓漢式
public class Singleton {
private final static Singleton instance = new Singleton();
//私有化構(gòu)造器
private Singleton(){}
//共有靜態(tài)方法吩翻,對(duì)外暴露獲取單例對(duì)象
public static Singleton getInstance(){
return instance;
}
}
可以看到餓漢式是在聲明靜態(tài)對(duì)象時(shí)就已經(jīng)初始化了兜看,如果沒(méi)有使用單例對(duì)象的情況下,就會(huì)造成不必要的內(nèi)存開(kāi)銷(xiāo)狭瞎。
懶漢式
懶漢式是聲明一個(gè)靜態(tài)對(duì)象细移,在第一次調(diào)用getInstance()
時(shí)進(jìn)行初始化
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
與餓漢式不同的地方不僅僅是單例對(duì)象初始化的時(shí)機(jī),會(huì)發(fā)現(xiàn)getInstance()
方法前添加了synchronized
關(guān)鍵字熊锭,也就是getInstance()
是一個(gè)同步方法弧轧,以此來(lái)保證多線程情況下單例對(duì)象的唯一。
相對(duì)的碗殷,每次調(diào)用getInstance()
都會(huì)進(jìn)行同步精绎,就會(huì)消耗不必要的資源,也是懶漢式存在的最大問(wèn)題亿扁。
Double Check Lock(DCL)
DCL方式實(shí)現(xiàn)單力模式的優(yōu)點(diǎn)在于既能在需要時(shí)才初始化對(duì)象捺典,又能保證線程安全鸟廓,而且在對(duì)象初始化之后調(diào)用getInstance()
不進(jìn)行同步
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(null == instance){
synchronized(Singleton.class){
if(null == instance){
instance = new Singleton();
}
}
}
return instance;
}
}
可以看到getInstance()
方法中對(duì)instance進(jìn)行了兩次判空从祝,第一次判斷是為了判斷不必要的同步,第二次判斷是為了在null的情況下穿件實(shí)例引谜;同時(shí)instance對(duì)象前面還添加了volatile
關(guān)鍵字牍陌,如果不使用volatile
關(guān)鍵字的話無(wú)法保證instance的原子性,這里涉及到instance = new Singleton();
語(yǔ)句不是一個(gè)原子操作员咽。
這句代碼最終會(huì)被編譯成多條匯編指令毒涧,大致做了3件事:
- 給Singleton的實(shí)力分配內(nèi)存
- 調(diào)用
Singleton()
的構(gòu)造函數(shù),初始化成員字段 - 將
instance
對(duì)象指向分配的內(nèi)存空間(此時(shí)instance
就不是null了)
由于Java編譯器允許處理器亂序執(zhí)行贝室,以及JDK1.5之前JMM(Java Memory Model契讲,即java內(nèi)存模型)中得Cache、寄存器到主內(nèi)存回寫(xiě)順序的規(guī)定滑频,第二和第三的順序是無(wú)法保證捡偏。也就是說(shuō)執(zhí)行順序可能是1-2-3或者是1-3-2。如果是后者峡迷,并在3執(zhí)行完成银伟、2為執(zhí)行前,備切換到線程B绘搞,這時(shí)候instance
已經(jīng)在線程A中執(zhí)行過(guò)了3彤避,instance
已經(jīng)是非空了,所以線程B直接取走instance
使用時(shí)會(huì)出錯(cuò)夯辖,導(dǎo)致DCL模式失效琉预,而且這種情況難以重現(xiàn)的錯(cuò)誤很可能會(huì)隱藏很久。
在JDK1.5之后蒿褂,調(diào)整了JVM圆米,具體化了volatile
關(guān)鍵字尖阔。所以在JDK1,5之后的版本在instance
前添加volatile
關(guān)鍵字保證每次都是從主內(nèi)存中讀取就可以使用DCL模式來(lái)完成代理模式了榨咐。當(dāng)然介却,volatile
或多或少會(huì)影響到性能,考慮到正確性這點(diǎn)性能的犧牲還是值得的块茁。
DCL模式能夠在絕大多數(shù)場(chǎng)景下保證單例對(duì)象的唯一性齿坷,資源利用率高,只有第一次加載時(shí)反應(yīng)稍慢数焊,一般能夠滿足需求
靜態(tài)內(nèi)部類(lèi)
在某些情況下DCL模式會(huì)出現(xiàn)時(shí)效的問(wèn)題永淌,于是邊有了靜態(tài)內(nèi)部類(lèi)的實(shí)現(xiàn)方式
public class Singleton {
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
/**靜態(tài)內(nèi)部類(lèi)*/
private static class SingletonHolder{
public static final Singleton instance = new Singleton();
}
}
當(dāng)?shù)谝淮渭虞dSingleton
類(lèi)時(shí)并不會(huì)初始化instance
,只有在第一次調(diào)用getInstance()
方法是回初始化佩耳。第一次調(diào)用getInstance()
方法導(dǎo)致虛擬機(jī)加載SingletonHolder
類(lèi)遂蛀,這種方式嫩確保線程安全,也能確保單例對(duì)象的唯一性干厚,同時(shí)也延遲了單例對(duì)象的實(shí)例化李滴,所以推薦使用這種實(shí)現(xiàn)方式。
枚舉單例
public enmu SingletonEnum {
INSTANCE;
}
就是這么簡(jiǎn)單粗暴蛮瞄,其實(shí)最大優(yōu)點(diǎn)在于關(guān)鍵點(diǎn)的第4點(diǎn)所坯,即是反序列化也不會(huì)重新生成新的實(shí)例。
通過(guò)序列化可以將單例對(duì)象寫(xiě)到磁盤(pán)挂捅,然后在讀取出來(lái)芹助,即使構(gòu)造函數(shù)是私有的,反序列化時(shí)依然可以通過(guò)特殊的方式創(chuàng)建一個(gè)新的實(shí)例闲先。反序列化操作提供了一個(gè)很特別的鉤子函數(shù)状土,類(lèi)中具有一個(gè)私有的、被實(shí)例化的方法readResolve()
伺糠,這個(gè)方法可以讓開(kāi)發(fā)人員控制對(duì)象的反序列化蒙谓。上述幾個(gè)實(shí)例中如果要避免反序列化是重新生成對(duì)象,必須加入如下方法:
private Object readResolve() throws ObjectStreamException{
return instance;
}
使用容器實(shí)現(xiàn)單例模式
public class SingletonManager{
private static Map<String,Object> objMap = new HashMap<>();
private SingletonManager(){}
public static void registerService(String key,Object instance){
if(!objMap.containsKey(key)){
objMap.put(key,instance);
}
}
public static Object getService(String key){
return objMap.get(key);
}
}
這種方式使得我們可以管理多種類(lèi)型的單例退盯,并且在使用時(shí)可以統(tǒng)一的接口進(jìn)行操作彼乌,降低了使用成本,同時(shí)隱藏了具體實(shí)現(xiàn)降低了耦合渊迁。
其實(shí)Android中LayoutInflater就是以這種方式實(shí)現(xiàn)的慰照。
3.總結(jié)
不管哪種形式實(shí)現(xiàn)單例模式,核心原理都是那四個(gè)關(guān)鍵點(diǎn)琉朽,具體選擇哪種實(shí)現(xiàn)方式取決于項(xiàng)目本身以及具體的開(kāi)發(fā)環(huán)境等等毒租。
而對(duì)于客戶(hù)端來(lái)說(shuō)通常沒(méi)有高并發(fā)的情況,推薦使用DCL模式或者是靜態(tài)內(nèi)部類(lèi)的方式實(shí)現(xiàn)箱叁。
優(yōu)點(diǎn)
- 只存在一個(gè)實(shí)例墅垮,減少了內(nèi)存開(kāi)支惕医,減少了系統(tǒng)的性能開(kāi)銷(xiāo)
- 避免對(duì)資源的多重占用
- 全局的訪問(wèn)點(diǎn),優(yōu)化和共享資源訪問(wèn)
缺點(diǎn)
- 沒(méi)有接口算色,難擴(kuò)展抬伺,只能修改代碼
- 如果持有Context容易導(dǎo)致內(nèi)存泄露(需要傳遞Context的話最好是Application Context)