1. 簡介
??單例模式確保某一個類只有一個實例放案,而且自行實例化并向整個系統(tǒng)提供這個實例姚建,這個類稱為單例類,它提供全局訪問的方法吱殉。
??單例模式的要點有三個:一是某個類只能有一個實例掸冤;二是它必須自行創(chuàng)建這個實例;三是它必須自行向整個系統(tǒng)提供這個實例友雳。單例模式是一種對象創(chuàng)建型模式稿湿。單例模式又名單件模式或單態(tài)模式。
在單例模式的實現(xiàn)過程中押赊,需要注意如下三點:
- 單例類的構(gòu)造函數(shù)為私有(即無法創(chuàng)建對象)饺藤;
- 提供一個自身的靜態(tài)私有成員變量;
- 提供一個公有的靜態(tài)工廠方法流礁。
優(yōu)點:
- 提供了對唯一實例的受控訪問涕俗。因為單例類封裝了它的唯一實例,所以它可以嚴(yán)格控制客戶怎樣以及何時訪問它神帅,并為設(shè)計及開發(fā)團(tuán)隊提供了共享的概念再姑。
- 由于在系統(tǒng)內(nèi)存中只存在一個對象,因此可以節(jié)約系統(tǒng)資源找御,對于一些需要頻繁創(chuàng)建和銷毀的對象元镀,單例模式無疑可以提高系統(tǒng)的性能。
- 允許可變數(shù)目的實例霎桅。我們可以基于單例模式進(jìn)行擴(kuò)展栖疑,使用與單例控制相似的方法來獲得指定個數(shù)的對象實例。
缺點:
- 由于單例模式中沒有抽象層滔驶,因此單例類的擴(kuò)展有很大的困難遇革。
- 單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”。因為單例類既充當(dāng)了工廠角色澳淑,提供了工廠方法比原,同時又充當(dāng)了產(chǎn)品角色,包含一些業(yè)務(wù)方法杠巡,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起量窘。
- 濫用單例將帶來一些負(fù)面問題,如為了節(jié)省資源將數(shù)據(jù)庫連接池對象設(shè)計為單例類氢拥,可能會導(dǎo)致共享連接池對象的程序過多而出現(xiàn)連接池溢出蚌铜;現(xiàn)在很多面向?qū)ο笳Z言(如Java、C#)的運行環(huán)境都提供了自動垃圾回收的技術(shù)嫩海,因此冬殃,如果實例化的對象長時間不被利用,系統(tǒng)會認(rèn)為它是垃圾叁怪,會自動銷毀并回收資源审葬,下次利用時又將重新實例化,這將導(dǎo)致對象狀態(tài)的丟失奕谭。
2. 單例實現(xiàn)
??常見的單例實現(xiàn)方式有五種:餓漢式涣觉、懶漢式、雙重檢測鎖式血柳、靜態(tài)內(nèi)部類式和枚舉單例官册,根據(jù)不同的業(yè)務(wù)場景選用不同的單例實現(xiàn)方式。
2.1 餓漢式
??線程安全,調(diào)用效率高,但是不能延遲加載难捌。
public class Singleton1 {
// 私有構(gòu)造膝宁,不可用于對象創(chuàng)建
private Singleton1() {}
// 靜態(tài)對象即類對象,全局唯一根吁,類加載時即進(jìn)行初始化员淫,保證單一
private static Singleton1 single = new Singleton1();
// 靜態(tài)工廠方法
public static Singleton1 getInstance() {
return single;
}
}
??餓漢式單例在類加載初始化時就創(chuàng)建好一個靜態(tài)的對象供外部使用,除非系統(tǒng)重啟击敌,這個對象不會改變介返,所以本身就是線程安全的。
??Singleton通過將構(gòu)造方法限定為private避免了類在外部被實例化愚争,在同一個虛擬機(jī)范圍內(nèi),Singleton的唯一實例只能通過getInstance()方法訪問(事實上通過Java反射機(jī)制是能夠?qū)嵗瘶?gòu)造方法為private的類的挤聘,那基本上會使所有的Java單例實現(xiàn)失效)轰枝。
2.2 懶漢式
??非線程安全,調(diào)用效率高,可延遲加載。
public class Singleton2 {
// 私有構(gòu)造
private Singleton2() {}
private static Singleton2 single = null;
public static Singleton2 getInstance() {
// 多線程并發(fā)破壞單例的設(shè)計原則组去,不安全
if(single == null){
single = new Singleton2();
}
return single;
}
}
??該示例在多線程環(huán)境下會產(chǎn)生多個single對象鞍陨,在下面雙重校驗?zāi)J接枰愿倪M(jìn)。
2.3 雙重檢測鎖式(double check)
??雙重校驗鎖DCL(double checked locking)
public class Singleton3 {
// 私有構(gòu)造
private Singleton3() {}
private volatile static Singleton3 single = null;
public static Singleton3 getInstance() {
// 等同于 synchronized public static Singleton3 getInstance()
synchronized(Singleton3.class){
// 注意:里面的判斷是一定要加的,否則出現(xiàn)線程安全問題
if(single == null){
single = new Singleton3();
}
}
return single;
}
}
??在方法上加synchronized同步鎖或是用同步代碼塊對類加同步鎖诚撵,此種方式雖然解決了多個實例對象問題缭裆,但是該方式運行效率卻很低下。
??雙重校驗鎖式是線程安全的寿烟。然而澈驼,在jdk1.5之前的版本,許多JVM對于volatile關(guān)鍵字的實現(xiàn)會導(dǎo)致dcl(double check locking)失效筛武。所以缝其,在JDK1.5以前的DCL是不穩(wěn)定的,有時也可能創(chuàng)建多個實例徘六,在1.5以后開始提供volatile關(guān)鍵字修飾變量來達(dá)到穩(wěn)定效果内边。
??volatile對變量single的修飾必不可少,因為volatile保證了原子性和有序性待锈。如果沒有volatile漠其,則single = new Singleton3();的執(zhí)行很可能會被重排序。
禁止指令重排序(有序性)實例化一個對象其實可以分為三個步驟:
- (1)分配內(nèi)存空間竿音。
- (2)初始化對象和屎。
- (3)將內(nèi)存空間的地址賦值給對應(yīng)的引用。
但是由于操作系統(tǒng)可以對指令進(jìn)行重排序谍失,所以上面的過程也可能會變成如下過程:
- (1)分配內(nèi)存空間眶俩。
- (2)將內(nèi)存空間的地址賦值給對應(yīng)的引用。
- (3)初始化對象快鱼。
??如果是這個流程颠印,多線程環(huán)境下就可能將一個未初始化的對象引用暴露出來,從而導(dǎo)致不可預(yù)料的結(jié)果(如題目的描述抹竹,這里就是因為 instance = new Singleton3(); 不是原子操作线罕,編譯器存在指令重排,從而存在線程1 創(chuàng)建實例后(初始化未完成)窃判,線程2 判斷對象不為空(因為已經(jīng)有了地址钞楼,所以判定為非空)后對其操作,但實際對象仍為空(沒有進(jìn)行初始化)袄琳,造成錯誤)询件。因此,為了防止這個過程的重排序唆樊,我們需要將變量設(shè)置為volatile類型的變量宛琅,volatile的禁止重排序保證了操作的有序性。
??Singleton對象的內(nèi)存可見性 這里由于synchronized鎖的是Singleton.class對象逗旁,而不是Singleton對象嘿辟,所以synchronized只能保證Singleton.class對象的內(nèi)存可見性,但并不能保證Singleton對象的內(nèi)存可見性;這里用volatile聲明Singleton红伦,可以保證Singleton對象的內(nèi)存可見性英古。這一點作用也是非常重要的(如題目的描述,避免因為線程1 創(chuàng)建實例后還只存在自己線程的工作內(nèi)存昙读,未更新到主存召调。線程 2 判斷對象為空,創(chuàng)建實例箕戳,從而存在多實例錯誤)某残。
2.4 靜態(tài)內(nèi)部類
??線程安全,調(diào)用效率高陵吸,可以延遲加載玻墅。
public class Singleton5 {
// 私有構(gòu)造
private Singleton5() {}
// 靜態(tài)內(nèi)部類
private static class InnerObject{
private static Singleton5 single = new Singleton6();
}
public static Singleton5 getInstance() {
return InnerObject.single;
}
}
??和餓漢式一樣采用的是classLoader機(jī)制,保證了線程安全問題壮虫,但不同的是澳厢,靜態(tài)內(nèi)部類同樣滿足懶加載(當(dāng)調(diào)用getInsstance()方法時,實例才會被創(chuàng)建)囚似,靜態(tài)內(nèi)部類即使Singleton類被加載也不會創(chuàng)建單例對象剩拢,除非調(diào)用里面的getInstance()方法。因為當(dāng)Singleton類被加載時其靜態(tài)內(nèi)部類SingletonHolder沒有被主動使用饶唤。只有當(dāng)調(diào)用getInstance方法時徐伐,才會裝載SingletonHolder類,從而實例化單例對象。
2.5 枚舉單例
??線程安全募狂,調(diào)用效率高办素,不能延遲加載,可以天然的防止反射和反序列化調(diào)用祸穷。單例的枚舉實現(xiàn)在《Effective Java》中有提到性穿,因為其功能完整、使用簡潔雷滚、無償?shù)靥峁┝诵蛄谢瘷C(jī)制需曾、在面對復(fù)雜的序列化或者反射攻擊時仍然可以絕對防止多次實例化等優(yōu)點,單元素的枚舉類型被作者認(rèn)為是實現(xiàn)Singleton的最佳方法祈远。
??不可能出現(xiàn)序列化呆万、反射產(chǎn)生對象的漏洞,但是不能做到延遲加載车份,默認(rèn)的枚舉實例的創(chuàng)建是線程安全的谋减。
枚舉單例模式,有三個好處:
- 1.實例的創(chuàng)建線程安全,確保單例
- 2.防止被反射創(chuàng)建多個實例
- 3.沒有序列化的問題
枚舉類:
public enum DataSourceEnum {
DATASOURCE;
private DBConnection connection = null;
private DataSourceEnum() {
connection = new DBConnection();
}
public DBConnection getConnection() {
return connection;
}
}
客戶端調(diào)用示例:
public class Main {
public static void main(String[] args) {
DBConnection con1 = DataSourceEnum.DATASOURCE.getConnection();
DBConnection con2 = DataSourceEnum.DATASOURCE.getConnection();
System.out.println(con1 == con2);
}
}
返回true
把上面枚舉編譯后的字節(jié)碼反編譯躬充,得到的代碼如下:
public final class DataSourceEnum extends Enum<DataSourceEnum> {
public static final DataSourceEnum DATASOURCE;
public static DataSourceEnum[] values();
public static DataSourceEnum valueOf(String s);
static {};
}
線程安全問題:
- DATASOURCE 被聲明為 static 的逃顶,由類加載過程,可以知道虛擬機(jī)會保證一個類的<clinit>() 方法在多線程環(huán)境中被正確的加鎖充甚、同步以政。所以,枚舉實現(xiàn)是在實例化時是線程安全伴找。
序列化問題:
- Java規(guī)范中規(guī)定盈蛮,每一個枚舉類型極其定義的枚舉變量在JVM中都是唯一的,因此在枚舉類型的序列化和反序列化上技矮,Java做了特殊的規(guī)定抖誉。
- 在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結(jié)果中,反序列化的時候則是通過 java.lang.Enum 的 valueOf() 方法來根據(jù)名字查找枚舉對象衰倦。
- 也就是說袒炉,以下面枚舉為例,序列化的時候只將 DATASOURCE 這個名稱輸出樊零,反序列化的時候再通過這個名稱我磁,查找對于的枚舉類型,因此反序列化后的實例也會和之前被序列化的對象實例相同驻襟。
2.6 破壞單例模式的方法及解決辦法
1夺艰、除枚舉方式外,其他方法都會通過反射的方式破壞單例,反射是通過調(diào)用構(gòu)造方法生成新的對象,所以如果我們想要阻止單例破壞沉衣,可以在構(gòu)造方法中進(jìn)行判斷郁副,若已有實例,則阻止生成新的實例,解決辦法如下
private SingletonObject1() {
if (instance != null) {
throw new RuntimeException("\"實例已經(jīng)存在豌习,請通過 getInstance()方法獲取\"");
}
}
2存谎、如果單例類實現(xiàn)了序列化接口Serializable,就可以通過反序列化破壞單例,所以我們可以不實現(xiàn)序列化接口,如果非得實現(xiàn)序列化接口斑鸦,可以重寫反序列化方法readResolve(),反序列化時直接返回相關(guān)單例對象愕贡。
public Object readResolve() throws ObjectStreamException {
return instance;
}
參考: