目錄:
文章目錄
前言
一 單例模式簡介
1.1 定義
1.2 為什么要用單例模式呢?
1.3 為什么不使用全局變量確保一個類只有一個實(shí)例呢反砌?
二 單例的模式的實(shí)現(xiàn)
2.1 餓漢方式(線程安全)
2.2 懶漢式(非線程安全和synchronized關(guān)鍵字線程安全版本 )
2.3 懶漢式(雙重檢查加鎖版本)
2.4 懶漢式(登記式/靜態(tài)內(nèi)部類方式)
2.5 餓漢式(枚舉方式)
2.6 總結(jié)
前言
初遇設(shè)計模式在上個寒假猎拨,當(dāng)時把每個設(shè)計模式過了一遍膀藐,對設(shè)計模式有了一個最初級的了解屠阻。這個學(xué)期借了幾本設(shè)計模式的書籍看,聽了老師的設(shè)計模式課额各,對設(shè)計模式算是有個更進(jìn)一步的認(rèn)識国觉。后面可能會不定期更新一下自己對于設(shè)計模式的理解。每個設(shè)計模式看似很簡單臊泰,實(shí)則想要在一個完整的系統(tǒng)中應(yīng)用還是非常非常難的蛉加。然后我的水品也非常非常有限,代碼量也不是很多缸逃,只能通過閱讀書籍、思考別人的編碼經(jīng)驗(yàn)以及結(jié)合自己的編碼過程中遇到的問題來總結(jié)厂抽。
怎么用->怎么用才好->怎么與其他模式結(jié)合使用需频,我想這是每個開發(fā)人員都需要逾越的一道鴻溝。
一 單例模式簡介
1.1 定義
保證一個類僅有一個實(shí)例筷凤,并提供一個訪問它的全局訪問點(diǎn)昭殉。
1.2 為什么要用單例模式呢?
在我們的系統(tǒng)中藐守,有一些對象其實(shí)我們只需要一個挪丢,比如說:線程池、緩存卢厂、對話框乾蓬、注冊表、日志對象慎恒、充當(dāng)打印機(jī)任内、顯卡等設(shè)備驅(qū)動程序的對象。事實(shí)上融柬,這一類對象只能有一個實(shí)例死嗦,如果制造出多個實(shí)例就可能會導(dǎo)致一些問題的產(chǎn)生,比如:程序的行為異常粒氧、資源使用過量越除、或者不一致性的結(jié)果。
簡單來說使用單例模式可以帶來下面幾個好處:
對于頻繁使用的對象外盯,可以省略創(chuàng)建對象所花費(fèi)的時間摘盆,這對于那些重量級對象而言,是非趁殴郑可觀的一筆系統(tǒng)開銷骡澈;
由于 new 操作的次數(shù)減少,因而對系統(tǒng)內(nèi)存的使用頻率也會降低掷空,這將減輕 GC 壓力肋殴,縮短 GC 停頓時間囤锉。
1.3 為什么不使用全局變量確保一個類只有一個實(shí)例呢?
我們知道全局變量分為靜態(tài)變量和實(shí)例變量护锤,靜態(tài)變量也可以保證該類的實(shí)例只存在一個官地。
只要程序加載了類的字節(jié)碼,不用創(chuàng)建任何實(shí)例對象烙懦,靜態(tài)變量就會被分配空間驱入,靜態(tài)變量就可以被使用了。
但是氯析,如果說這個對象非常消耗資源亏较,而且程序某次的執(zhí)行中一直沒用,這樣就造成了資源的浪費(fèi)掩缓。利用單例模式的話雪情,我們就可以實(shí)現(xiàn)在需要使用時才創(chuàng)建對象,這樣就避免了不必要的資源浪費(fèi)你辣。 不僅僅是因?yàn)檫@個原因巡通,在程序中我們要盡量避免全局變量的使用,大量使用全局變量給程序的調(diào)試舍哄、維護(hù)等帶來困難宴凉。
二 單例的模式的實(shí)現(xiàn)
通常單例模式在Java語言中,有兩種構(gòu)建方式:
餓漢方式表悬。指全局的單例實(shí)例在類裝載時構(gòu)建
懶漢方式弥锄。指全局的單例實(shí)例在第一次被使用時構(gòu)建。
不管是那種創(chuàng)建方式签孔,它們通常都存在下面幾點(diǎn)相似處:
單例類必須要有一個 private 訪問級別的構(gòu)造函數(shù)叉讥,只有這樣,才能確保單例不會在系統(tǒng)中的其他代碼內(nèi)被實(shí)例化;
instance 成員變量和 uniqueInstance 方法必須是 static 的饥追。
2.1 餓漢方式(線程安全)
public class Singleton {
//在靜態(tài)初始化器中創(chuàng)建單例實(shí)例图仓,這段代碼保證了線程安全
private static Singleton uniqueInstance = new Singleton();
//Singleton類只有一個構(gòu)造方法并且是被private修飾的,所以用戶無法通過new方法創(chuàng)建該對象實(shí)例
private Singleton(){}
public static Singleton getInstance(){
return uniqueInstance;
}
}
所謂 “餓漢方式” 就是說JVM在加載這個類時就馬上創(chuàng)建此唯一的單例實(shí)例但绕,不管你用不用救崔,先創(chuàng)建了再說,如果一直沒有被使用捏顺,便浪費(fèi)了空間六孵,典型的空間換時間,每次調(diào)用的時候幅骄,就不需要再判斷劫窒,節(jié)省了運(yùn)行時間。
2.2 懶漢式(非線程安全和synchronized關(guān)鍵字線程安全版本 )
public class Singleton {
private static Singleton uniqueInstance;
private Singleton (){
}
//沒有加入synchronized關(guān)鍵字的版本是線程不安全的
public static Singleton getInstance() {
//判斷當(dāng)前單例是否已經(jīng)存在拆座,若存在則返回主巍,不存在則再建立單例
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
所謂 “ 懶漢式” 就是說單例實(shí)例在第一次被使用時構(gòu)建冠息,而不是在JVM在加載這個類時就馬上創(chuàng)建此唯一的單例實(shí)例。
但是上面這種方式很明顯是線程不安全的孕索,如果多個線程同時訪問getInstance()方法時就會出現(xiàn)問題逛艰。如果想要保證線程安全,一種比較常見的方式就是在getInstance() 方法前加上synchronized關(guān)鍵字搞旭,如下:
public static synchronized Singleton getInstance() {
if (instance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
我們知道synchronized關(guān)鍵字偏重量級鎖散怖。雖然在JavaSE1.6之后synchronized關(guān)鍵字進(jìn)行了主要包括:為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優(yōu)化之后執(zhí)行效率有了顯著提升。
但是在程序中每次使用getInstance() 都要經(jīng)過synchronized加鎖這一層肄渗,這難免會增加getInstance()的方法的時間消費(fèi)镇眷,而且還可能會發(fā)生阻塞。我們下面介紹到的 雙重檢查加鎖版本 就是為了解決這個問題而存在的翎嫡。
2.3 懶漢式(雙重檢查加鎖版本)
利用雙重檢查加鎖(double-checked locking)偏灿,首先檢查是否實(shí)例已經(jīng)創(chuàng)建,如果尚未創(chuàng)建钝的,“才”進(jìn)行同步。這樣以來铆遭,只有一次同步硝桩,這正是我們想要的效果。
public class Singleton {
//volatile保證枚荣,當(dāng)uniqueInstance變量被初始化成Singleton實(shí)例時碗脊,多個線程可以正確處理uniqueInstance變量
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getInstance() {
//檢查實(shí)例,如果不存在橄妆,就進(jìn)入同步代碼塊
if (uniqueInstance == null) {
//只有第一次才徹底執(zhí)行這里的代碼
synchronized(Singleton.class) {
//進(jìn)入同步代碼塊后衙伶,再檢查一次,如果仍是null害碾,才創(chuàng)建實(shí)例
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
很明顯矢劲,這種方式相比于使用synchronized關(guān)鍵字的方法,可以大大減少getInstance() 的時間消費(fèi)慌随。
我們上面使用到了volatile關(guān)鍵字來保證數(shù)據(jù)的可見性芬沉,關(guān)于volatile關(guān)鍵字的內(nèi)容可以看我的這篇文章:
《Java多線程學(xué)習(xí)(三)volatile關(guān)鍵字》: https://blog.csdn.net/qq_34337272/article/details/79680771
注意: 雙重檢查加鎖版本不適用于1.4及更早版本的Java。
1.4及更早版本的Java中阁猜,許多JVM對于volatile關(guān)鍵字的實(shí)現(xiàn)會導(dǎo)致雙重檢查加鎖的失效丸逸。
2.4 懶漢式(登記式/靜態(tài)內(nèi)部類方式)
靜態(tài)內(nèi)部實(shí)現(xiàn)的單例是懶加載的且線程安全。
只有通過顯式調(diào)用 getInstance 方法時剃袍,才會顯式裝載 SingletonHolder 類黄刚,從而實(shí)例化 instance(只有第一次使用這個單例的實(shí)例的時候才加載,同時不會有線程安全問題)民效。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
2.5 餓漢式(枚舉方式)
這種實(shí)現(xiàn)方式還沒有被廣泛采用憔维,但這是實(shí)現(xiàn)單例模式的最佳方法涛救。 它更簡潔,自動支持序列化機(jī)制埋同,絕對防止多次實(shí)例化 (如果單例類實(shí)現(xiàn)了Serializable接口州叠,默認(rèn)情況下每次反序列化總會創(chuàng)建一個新的實(shí)例對象,關(guān)于單例與序列化的問題可以查看這一篇文章《單例與序列化的那些事兒》)凶赁,同時這種方式也是《Effective Java 》以及《Java與模式》的作者推薦的方式咧栗。
public enum Singleton {
//定義一個枚舉的元素,它就是 Singleton 的一個實(shí)例
INSTANCE;
public void doSomeThing() {
System.out.println("枚舉方法實(shí)現(xiàn)單例");
}
}
使用方法:
public class ESTest {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.doSomeThing();//output:枚舉方法實(shí)現(xiàn)單例
}
}
《Effective Java 中文版 第二版》
這種方法在功能上與公有域方法相近虱肄,但是它更加簡潔致板,無償提供了序列化機(jī)制,絕對防止多次實(shí)例化咏窿,即使是在面對復(fù)雜序列化或者反射攻擊的時候斟或。雖然這種方法還沒有廣泛采用,但是單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法集嵌。 —-《Effective Java 中文版 第二版》
《Java與模式》
《Java與模式》中萝挤,作者這樣寫道,使用枚舉來實(shí)現(xiàn)單實(shí)例控制會更加簡潔根欧,而且無償?shù)靥峁┝诵蛄谢瘷C(jī)制怜珍,并由JVM從根本上提供保障,絕對防止多次實(shí)例化凤粗,是更簡潔酥泛、高效、安全的實(shí)現(xiàn)單例的方式嫌拣。
2.6 總結(jié)
我們主要介紹到了以下幾種方式實(shí)現(xiàn)單例模式:
- 餓漢方式(線程安全)
- 懶漢式(非線程安全和synchronized關(guān)鍵字線程安全版本)
- 懶漢式(雙重檢查加鎖版本)
- 懶漢式(登記式/靜態(tài)內(nèi)部類方式)
- 餓漢式(枚舉方式)
參考:
《Head First 設(shè)計模式》
《Effective Java 中文版 第二版》