單例模式的定義
確保某一個類只有一個實(shí)例斤斧,而且自行實(shí)例化并向整個系統(tǒng)提供這個實(shí)例早抠。
單例模式的使用場景
確保某個類有且只有一個對象的場景,避免產(chǎn)生多個對象消耗過多的資源撬讽。例如創(chuàng)建一個對象需要消耗的資源過多蕊连,如要訪問 IO 和數(shù)據(jù)庫等資源悬垃。
(1)Client——高層客戶端;
(2)Singleton——單例類甘苍;
實(shí)現(xiàn)單例模式主要有如下幾個關(guān)鍵點(diǎn):
(1)構(gòu)造函數(shù)不對外開放尝蠕,一般為 Private;
(2)通過一個靜態(tài)方法或者枚舉返回單例類對象载庭;
(3)確保單例類的對象有且只有一個看彼,尤其是在多線程環(huán)境下;
(4)確保單例類對象在反序列化時不會重新構(gòu)建對象囚聚。
單例模式的簡單實(shí)例
(1)懶漢模式:
public class Singleton {
private static Singleton sInstance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
}
getInstance() 方法中添加了 synchronized 關(guān)鍵字靖榕,保證方法在多線程情況下單例對象的唯一性。
懶漢單例模式的優(yōu)點(diǎn)是單例只有在使用時才會被實(shí)例化顽铸,在一定程度下節(jié)約了資源茁计;缺點(diǎn)是第一次加載時需要及時進(jìn)行實(shí)例化,反應(yīng)稍慢谓松,最大的問題是每次調(diào)用 getInstance 都進(jìn)行同步星压,造成不必要的同步開銷。一般不建議使用毒返。
(2)Double Check Lock (DCL)實(shí)現(xiàn)單例
public class Singleton {
private static Singleton sInstance = null;
private Singleton() {}
public void doSomething() {
System.out.println("do sth.");
}
public static Singleton getInstance() {
if (sInstance == null) {
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}
}
getInstance 方法中對 sIntance 進(jìn)行了兩次判空:
- 第一層避免了不必要的同步租幕;
- 第二層是為了在 null 的情況下創(chuàng)建實(shí)例。
假設(shè)線程 A 執(zhí)行到了 sInstance = new Singleton()語句拧簸,這里看起來是一句代碼劲绪,但實(shí)際上它并不是一個原子操作,這句代碼最終會被編譯成多條匯編指令盆赤,它大致做了 3 件事:
(1)給 Singleton 的實(shí)例分配內(nèi)存贾富;
(2)調(diào)用 Singleton 的構(gòu)造函數(shù),初始化成員字段牺六;
(3)將 sInstance 對象指向分配的內(nèi)存空間(此時 sInstance 就是不 null )
但是颤枪,由于 Java 編譯器允許處理器亂序執(zhí)行,以及 JDK1.5之前 JMM(Java Memory Model淑际,即內(nèi)存模型)中 Cache畏纲、寄存器到主內(nèi)存回寫順序的規(guī)定,上面的第二和第三執(zhí)行順序是無法保證的春缕,可能是 1-2-3盗胀,也可能是1-3-2。如果是后者锄贼,3 執(zhí)行完畢票灰,2 未執(zhí)行之前,被切換到了線程 B,這時候 sInstance 以及不為 null屑迂,所以線程 B 會直接取走 sInstance浸策,又因?yàn)闆]有執(zhí)行 2 所以成員字段沒有初始化,再使用時就會出錯惹盼,這就是 DCL 失效問題庸汗,而且這種難以跟蹤難以重現(xiàn)的錯誤很可能會隱藏很久。
在 JDK 1.5 之后逻锐,SUN 官方調(diào)整了 JVM夫晌,具體化了 volatile 關(guān)鍵字,因此只需要將 sInstance 的定義改成private volatile static Singleton sInstance = null;
就可以保證 sInstance 對象每次都是從主內(nèi)存中讀取昧诱,volatile 或多或少會影響到性能,但是考慮程序的正確性所袁,這點(diǎn)犧牲還是值得的盏档。
DCL 的優(yōu)點(diǎn):資源利用率高;缺點(diǎn):第一次加載時反應(yīng)稍慢燥爷,在高并發(fā)環(huán)境下也有很小概率的缺陷蜈亩。DCL 單例模式是使用最多的單例實(shí)現(xiàn)方式,基本能滿足需求前翎。
(3)靜態(tài)內(nèi)部類單例模式
DCL 雖然在一定程度上解決了資源消耗稚配、多余的同步、線程安全等問題港华,但是它還是存在某些情況下出現(xiàn)失效的問題道川,這個問題被稱為雙重檢查鎖定(DCL)失效,建議使用如下的代碼代替:
public class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.sInstance;
}
/**
* 靜態(tài)內(nèi)部類
*/
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
當(dāng)?shù)谝淮渭虞d Singleton 類時并不會初始化 sInstance立宜,只有在第一次調(diào)用 getInstance 方法時才會初始化 sInstance冒萄。第一次調(diào)用 getInstance 方法會導(dǎo)致虛擬機(jī)加載 SingletonHolder 類,這種方式不僅能夠確保線程安全橙数,也能保證單例對象的唯一性尊流,同時也延遲了單例的實(shí)例化,所以這是推薦使用的單例模式實(shí)現(xiàn)方式灯帮。
(4)枚舉單例
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("");
}
}
寫法簡單是枚舉單例最大的有點(diǎn)崖技,枚舉在 Java 中與普通的類是一樣的。不僅能夠擁有自己的字段钟哥,還能夠有自己的方法迎献,最終的是默認(rèn)枚舉實(shí)例的創(chuàng)建是線程安全的,并且在任何情況下它都是一個單例瞪醋。
在上面(1)忿晕、(2)、(3)的三種單例模式實(shí)現(xiàn)中银受,當(dāng)一個單例的實(shí)例對象通過序列化被寫到磁盤践盼,然后再讀出來鸦采,從而獲得一個實(shí)例,即使構(gòu)造方法是私有的咕幻,反序列化依然可以通過特殊的途徑去創(chuàng)建類的一個新的實(shí)例渔伯,相當(dāng)于調(diào)用了該類的構(gòu)造函數(shù)(例如:繼承 Serializable,寫到磁盤是序列化操作肄程,讀取是反序列化操作)锣吼。反序列化操作提供了一個很特別的鉤子函數(shù),類中具體有一個私有的蓝厌、被實(shí)例化的方法 readReslove 方法玄叠,這個方法直接加入到單例模式中就可以讓開發(fā)人員控制對象的反序列話操作。(這應(yīng)該是底層的知識了拓提,懵逼~~~)
private Object readReslove()throws ObjectStreamException {
return sInstance;
}
也就是在 readReslove 方法中將 sInstance 對象返回读恃,而不是默認(rèn)生成一個新的對象,JDK 5 的 enum 類型系統(tǒng)已經(jīng)處理了這個readresolve的情況代态。
(5)使用容器實(shí)現(xiàn)單例模式
public class SingletonMnager {
private static Map<String, Object> objMap = new HashMap<>();
private SingletonMnager(){}
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);
}
}
在程序的初識寺惫,將多種單例類型注入到一個統(tǒng)一的管理類中,在使用時根據(jù) key 獲取對象對應(yīng)類型的對象蹦疑。這種方式使得我們可以管理多種類型的單例西雀,并且在使用時可以通過統(tǒng)一的接口進(jìn)行獲取操作,降低了用戶的使用成本歉摧,也對用戶因此了具體實(shí)現(xiàn)艇肴,降低了耦合度。
不管一那種形式實(shí)現(xiàn)單例模式判莉,它們的核心原理都是將構(gòu)造函數(shù)私有化豆挽,并且通過靜態(tài)方法獲取一個唯一的實(shí)例,選擇哪種實(shí)現(xiàn)方式取決于項(xiàng)目本身券盅,如是否復(fù)雜的并發(fā)環(huán)境帮哈、JDK 版本是否過低,單例對象的資源消耗等锰镀。