單例模式(Singleton Pattern)是 Java 中相對簡單的設(shè)計模式之一狸涌。這種類型的設(shè)計模式屬于創(chuàng)建型模式旦万,它提供了一種創(chuàng)建對象的最佳方式橄仆。
這種模式涉及到一個單一的類歌豺,該類負(fù)責(zé)創(chuàng)建自己的對象桅滋,同時確保只有單個對象被創(chuàng)建。這個類提供了一種訪問其唯一的對象的方式既绩,可以直接訪問概龄,不需要實例化該類的對象。
使用場景:
1饲握、要求生產(chǎn)唯一序列號私杜。
2、WEB 中的計數(shù)器救欧,不用每次刷新都在數(shù)據(jù)庫里加一次衰粹,用單例先緩存起來。
3笆怠、創(chuàng)建的一個對象需要消耗的資源過多铝耻,比如 I/O 與數(shù)據(jù)庫的連接等。
注意事項:getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多線程同時進入造成 instance 被多次實例化蹬刷。
實現(xiàn)1:懶漢式瓢捉,線程不安全
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
分析:這種方式是最容易想到的實現(xiàn)方式,但是這種實現(xiàn)存在一個很大大的問題:不支持多線程办成。因為沒有加鎖 synchronized泡态,所以嚴(yán)格意義上它并不算單例模式。
這種方式 lazy loading 很明顯迂卢,不要求線程安全某弦,在多線程不能正常工作。
實現(xiàn)2:懶漢式冷守,線程安全
基于實現(xiàn)1存在的線程不安全問題,可得到以下實現(xiàn):
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
分析:這種方式具備很好的 lazy loading惊科,能夠在多線程中很好的工作拍摇,但是,效率很低馆截,大多數(shù)情況下不需要同步充活。
優(yōu)點:第一次調(diào)用才初始化蜂莉,避免內(nèi)存浪費。
缺點:必須加鎖 synchronized 才能保證單例混卵,但加鎖會影響效率映穗。getInstance() 的性能對應(yīng)用程序不是很關(guān)鍵(該方法使用不太頻繁)。
實現(xiàn)3:懶漢式幕随,雙重檢驗鎖蚁滋,線程安全,性能較高
基于實現(xiàn)2中的性能問題赘淮,可得出以下線程安全且高性能的實現(xiàn):
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
分析:這種方式采用雙鎖機制辕录,即通過volatile關(guān)鍵字保證線程的內(nèi)存可見,確保singleton是否為null能及時刷新到各線程的工作內(nèi)存中梢卸,同時有在if條件內(nèi)部添加synchronized同步鎖安全走诞,在singleton實例為空時,才給線程添加同步鎖蛤高,以保證線程安全蚣旱,且在多線程情況下能保持高性能。
實現(xiàn)4:餓漢式戴陡,線程安全:
基于實現(xiàn)3中復(fù)雜的實現(xiàn)方式塞绿,可采用實現(xiàn)更為簡單的寫法,如下:
public Singleton(){
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
分析:這種方式比較常用猜欺,但容易產(chǎn)生垃圾對象位隶。
優(yōu)點:沒有加鎖,執(zhí)行效率會提高开皿。
缺點:類加載時就初始化涧黄,浪費內(nèi)存。
深入理解:它基于 classloder 機制避免了多線程的同步問題赋荆,即笋妥,類初始化階段由JLS保證是順序的,是非并發(fā)的窄潭,不過春宣,instance 在類裝載時就實例化,雖然導(dǎo)致類裝載的原因有很多種嫉你,在單例模式中大多數(shù)都是調(diào)用 getInstance 方法月帝, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時候初始化 instance 顯然沒有達到 lazy loading 的效果幽污。
實現(xiàn)5:靜態(tài)內(nèi)部類法嚷辅,懶加載,線程安全:
基于實現(xiàn)4中存在的資源耗費問題距误,可有以下解決方法來改善實現(xiàn):
public class Singleton{
private Singleton(){}
//創(chuàng)建內(nèi)部靜態(tài)類簸搞,第一次使用時才被加載
private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
分析:這種方式能達到雙檢鎖方式一樣的功效扁位,但實現(xiàn)更簡單。對靜態(tài)域使用延遲初始化趁俊,應(yīng)使用這種方式而不是雙檢鎖方式域仇。這種方式只適用于靜態(tài)域的情況,雙檢鎖方式可在實例域需要延遲初始化時使用寺擂。
深入理解:這種方式同樣利用了 classloder 機制來保證初始化 instance 時只有一個線程暇务,它跟第 4 種方式不同的是:第 4 種方式只要 Singleton 類被裝載了,那么 instance 就會被實例化(沒有達到 lazy loading 效果)沽讹,而這種方式是 Singleton 類被裝載了般卑,instance 不一定被初始化。因為 SingletonHolder 類沒有被主動使用爽雄,只有顯示通過調(diào)用 getInstance 方法時蝠检,才會顯示裝載 SingletonHolder 類,從而實例化 instance挚瘟。
試想叹谁,如果實例化 instance 很消耗資源,所以想讓它延遲加載乘盖,另外一方面焰檩,又不希望在 Singleton 類加載時就實例化,因為不能確保 Singleton 類還可能在其他的地方被主動使用從而被加載订框,那么這個時候?qū)嵗?instance 顯然是不合適的析苫。這個時候,這種方式相比第 4 種方式就顯得很合理穿扳。
延伸:虛擬機規(guī)范嚴(yán)格規(guī)定了5種情況下衩侥,必須對類(注意是類)進行初始化
- 遇到new,getstatic,putstatic,invokestatic這4條字節(jié)碼指令時,如果類沒有進行過初始化矛物,則需要先觸發(fā)其初始化茫死。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾履羞、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時候峦萎,以及調(diào)用一個類的靜態(tài)方法的時候。
- 使用java.lang.reflect包的方法對類進行反射調(diào)用的時候忆首,如果類沒有進行過初始化爱榔,則需要先觸發(fā)其初始化。
- 當(dāng)初始化一個類的時候糙及,如果發(fā)現(xiàn)其父類還沒有進行過初始化详幽,則需要先觸發(fā)其父類的初始化。
- 當(dāng)虛擬機啟動時丁鹉,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類)妒潭,虛擬機會先初始化這個主類。
- 當(dāng)使用jdk1.7動態(tài)語言支持時揣钦,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄雳灾,并且這個方法句柄所對應(yīng)的類沒有進行初始化,則需要先出觸發(fā)其初始化
實現(xiàn)6:枚舉法冯凹,懶加載谎亩,線程安全,實現(xiàn)簡單:
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
分析:這種實現(xiàn)方式還沒有被廣泛采用宇姚,但這是實現(xiàn)單例模式的最佳方法匈庭。它更簡潔,自動支持序列化機制浑劳,絕對防止多次實例化阱持。這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題魔熏,而且還自動支持序列化機制衷咽,防止反序列化重新創(chuàng)建新的對象,絕對防止多次實例化蒜绽。不過镶骗,由于 JDK1.5 之后才加入 enum 特性,用這種方式寫不免讓人感覺生疏躲雅,在實際工作中鼎姊,也很少用。不能通過 reflection attack 來調(diào)用私有構(gòu)造方法相赁。
總結(jié):
一般情況下相寇,不建議使用第 1 種和第 2 種懶漢方式,建議使用第 4 種餓漢方式噪生。只有在要明確實現(xiàn) lazy loading 效果時裆赵,才會使用第 5 種登記方式。如果涉及到反序列化創(chuàng)建對象時跺嗽,可以嘗試使用第 6 種枚舉方式战授。如果有其他特殊的需求,可以考慮使用第 3 種雙檢鎖方式桨嫁。
參考資料:
1植兰、阿里云大學(xué):https://edu.aliyun.com/lesson_471_4545?spm=5176.8764728.0.0.ZNKWmq#_4545
2、個人平時學(xué)習(xí)總結(jié)筆記璃吧。