概念
確保某一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化,并向整個(gè)系統(tǒng)提供一個(gè)訪問它的全局訪問點(diǎn),這個(gè)類稱為單例類。
特性
單例類只能有一個(gè)實(shí)例
單例類必須自行創(chuàng)建自己的唯一的實(shí)例
單例類必須給所有其他對(duì)象提供這一實(shí)例
應(yīng)用場(chǎng)景
- 系統(tǒng)只需要一個(gè)實(shí)例對(duì)象,如系統(tǒng)要求提供一個(gè)唯一的序列號(hào)生成器或資源管理器,或者需要考慮資源消耗太大而只允許創(chuàng)建一個(gè)對(duì)象
- 在整個(gè)項(xiàng)目中需要一個(gè)共享訪問點(diǎn)或共享數(shù)據(jù),除了該公共訪問點(diǎn),不能通過其他途徑訪問該實(shí)例
- 創(chuàng)建一個(gè)對(duì)象需要消耗的資源過多,如要訪問IO和數(shù)據(jù)庫(kù)等資源
- 需要定義大量的靜態(tài)常量和靜態(tài)方法(如工具類)的環(huán)境,可以采用單例模式(當(dāng)然,也可以直接聲明為static的方式)
- 資源共享的情況下,避免由于資源操作時(shí)導(dǎo)致的性能或損耗等。如上述中的日志文件,應(yīng)用配置,數(shù)據(jù)庫(kù)連接池。
- 控制資源的情況下,方便資源之間的互相通信。如線程池等。
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 由于單例模式在內(nèi)存中只有一個(gè)實(shí)例,減少了內(nèi)存開支,特別是一個(gè)對(duì)象需要頻繁地創(chuàng)建銷毀時(shí),而且創(chuàng)建或銷毀時(shí)性能又無法優(yōu)化,單例模式的優(yōu)勢(shì)就非常明顯
- 由于單例模式只生成一個(gè)實(shí)例,所以減少了系統(tǒng)的性能開銷,當(dāng)一個(gè)對(duì)象的產(chǎn)生需要比較多的資源的時(shí)候,如讀取配置,產(chǎn)生其他的依賴對(duì)象時(shí),可以通過在應(yīng)用啟動(dòng)的時(shí)候直接產(chǎn)生一個(gè)單例對(duì)象,然后用永久駐留內(nèi)存的方式來解決
- 單例模式可以避免對(duì)資源的多重占用,例如對(duì)一個(gè)寫文件動(dòng)作,由于只有一個(gè)實(shí)例存在內(nèi)存中,避免對(duì)同一個(gè)資源文件的同時(shí)寫操作
- 單例模式可以在系統(tǒng)設(shè)置全局的訪問點(diǎn),優(yōu)化和共享資源訪問,例如可以設(shè)計(jì)一個(gè)單例類,負(fù)責(zé)所有數(shù)據(jù)表的映射處理
缺點(diǎn):
- 單例模式一般沒有接口/抽象層,擴(kuò)展困難。
- 單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”,一個(gè)類應(yīng)該只實(shí)現(xiàn)一個(gè)邏輯,而不關(guān)心他是否是單例的,是不是要單例取決于環(huán)境。因?yàn)閱卫惣瘸洚?dāng)了工廠角色,提供了工廠方法,同時(shí)又充當(dāng)了產(chǎn)品角色,包含一些業(yè)務(wù)方法,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起。
- 現(xiàn)在很多面向?qū)ο笳Z言(如Java、C#)的運(yùn)行環(huán)境都提供了自動(dòng)垃圾回收的技術(shù),因此,如果實(shí)例化的共享對(duì)象長(zhǎng)時(shí)間不被利用,系統(tǒng)會(huì)認(rèn)為它是垃圾,會(huì)自動(dòng)銷毀并回收資源,下次利用時(shí)又將重新實(shí)例化,這將導(dǎo)致共享的單例對(duì)象狀態(tài)的丟失。
代碼實(shí)現(xiàn)
- 懶漢式(線程安全、線程不安全)
- 餓漢式(線程安全)
- 雙重檢查加鎖
- 靜態(tài)內(nèi)部類
- 枚舉單例
1. 懶漢式、線程不安全
這種方式是最基本的實(shí)現(xiàn)方式,這種實(shí)現(xiàn)最大的問題就是不支持多線程。因?yàn)闆]有加鎖synchronized,所以嚴(yán)格意義上它并不算單例模式。
與餓漢式的不同:不是一看到 instance 就初始化,飽漢要等到第一次使用的時(shí)候才初始化,不像餓漢一樣一見到 instance 就初始化,這也被稱為 懶加載,如果系統(tǒng)中很多這樣的類,顯然是懶加載的時(shí)候效率更高
public class Singleton {
// 4:定義一個(gè)變量來存儲(chǔ)創(chuàng)建好的類實(shí)例(關(guān)鍵點(diǎn):聲明單例對(duì)象是靜態(tài)的)
// 5:因?yàn)檫@個(gè)變量要在靜態(tài)方法中使用,所以需要加上static修飾
private static Singleton instance;
// 1:私有化構(gòu)造方法,好在內(nèi)部控制創(chuàng)建實(shí)例的數(shù)目,限制產(chǎn)生多個(gè)對(duì)象(關(guān)鍵點(diǎn):構(gòu)造函數(shù)是私有的)
private Singleton() {}
// 2:定義一個(gè)方法來為客戶端提供類實(shí)例
// 3:這個(gè)方法需要定義成類方法,也就是要加static
// 定義一個(gè)靜態(tài)方法來為客戶端提供類實(shí)例(全局訪問點(diǎn)),這樣就不需要先得到類實(shí)例
public static Singleton getInstance() {
// 6:判斷存儲(chǔ)實(shí)例的變量是否有值(關(guān)鍵點(diǎn):判斷單例對(duì)象是否已經(jīng)被構(gòu)造)
if(instance == null) {
// 6.1:如果沒有,就創(chuàng)建一個(gè)類實(shí)例,并把值賦值給存儲(chǔ)類實(shí)例的變量
instance = new Singleton();
}
// 6.2:如果有值,那就直接使用
return instance;
}
}
為什么這種實(shí)現(xiàn)是線程不安全的呢?如一個(gè)線程A執(zhí)行到singleton = new Singleton();這里,但還沒有獲得對(duì)象(對(duì)象初始化是需要時(shí)間的),第二個(gè)線程B也在執(zhí)行,執(zhí)行到if(singleton == null)判斷,那么線程B獲得判斷條件也是為真,于是繼續(xù)運(yùn)行下去,線程A獲得了一個(gè)對(duì)象,線程B也獲得了一個(gè)對(duì)象,在內(nèi)存中就出現(xiàn)兩個(gè)對(duì)象,造成單例模式的失效!!
2. 懶漢式、線程安全
第一次調(diào)用才初始化,避免內(nèi)存浪費(fèi)。絕對(duì)線程安全,但是效率很低,99%情況下不需要同步。必須加鎖 synchronized 才能保證單例,但加鎖會(huì)影響效率,并發(fā)性能極差,事實(shí)上完全退化到了串行。
public class Singleton {
private static Singleton singleton;
private Singleton() {}
// 加鎖 synchronized
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
或者也可以這樣:
public class Singleton {
private static Singleton instance = null; // 關(guān)鍵點(diǎn) 1:聲明單例對(duì)象是靜態(tài)的
private Singleton() {} // 關(guān)鍵點(diǎn) 0:構(gòu)造函數(shù)是私有的
private static Object obj = new Object();
public static Singleton GetInstance() { // 通過靜態(tài)方法來構(gòu)造對(duì)象
if (instance == null) { // 關(guān)鍵點(diǎn) 2:判斷單例對(duì)象是否已經(jīng)被構(gòu)造
lock(obj) { // 關(guān)鍵點(diǎn) 3:加線程鎖
instance = new Singleton();
}
}
return instance;
}
}
雖然這里判斷了一次單例對(duì)象是否已經(jīng)被構(gòu)造,但是由于某些情況下,可能有延遲加載或者緩存的原因,只有關(guān)鍵點(diǎn) 2 這一次判斷,仍然不能保證系統(tǒng)是否只創(chuàng)建了一個(gè)單例,也可能出現(xiàn)多個(gè)實(shí)例的情況。
3. 餓漢式
這種方式比較常用,但容易產(chǎn)生垃圾對(duì)象。
優(yōu)點(diǎn):沒有加鎖,執(zhí)行效率會(huì)提高。
缺點(diǎn):類加載時(shí)就初始化,浪費(fèi)內(nèi)存。
餓漢單例模式線程安全。
值得注意的時(shí),單線程環(huán)境下,餓漢與飽漢在性能上沒什么差別;但多線程環(huán)境下,由于飽漢需要加鎖,餓漢的性能反而更優(yōu)。
public class Singleton {
// 定義一個(gè)靜態(tài)變量來存儲(chǔ)創(chuàng)建好的類實(shí)例,直接在這里創(chuàng)建類實(shí)例,只會(huì)創(chuàng)建一次
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
也可以這樣寫(使用靜態(tài)初始化塊):
public class Singleton {
private static Singleton instance = new Singleton();
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
4. 雙重檢查鎖(DCL,即 double-checked locking)
沒有volatile修飾instance的雙重檢查鎖版本仍然是線程不安全的,由于指令重排序,你可能會(huì)得到 “半個(gè)對(duì)象”。
如果使用雙重檢查鎖定來實(shí)現(xiàn)懶漢式單例類,需要在靜態(tài)成員變量instance之前增加修飾符volatile,被volatile修飾的成員變量可以確保多個(gè)線程都能夠正確處理。
由于volatile關(guān)鍵字會(huì)屏蔽Java虛擬機(jī)所做的一些代碼優(yōu)化,可能會(huì)導(dǎo)致系統(tǒng)運(yùn)行效率降低,因此即使使用雙重檢查鎖定來實(shí)現(xiàn)單例模式也不是一種完美的實(shí)現(xiàn)方式。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
所以,在判斷單例實(shí)例是否被構(gòu)造時(shí),需要檢測(cè)兩次,在線程鎖之前判斷一次,在線程鎖之后判斷一次,再去構(gòu)造實(shí)例,這樣就萬無一失了。
或者也可以這樣:
public class Singleton {
private static Singleton instance = null; // 關(guān)鍵點(diǎn) 1:聲明單例對(duì)象是靜態(tài)的
private Singleton() {} // 關(guān)鍵點(diǎn) 0:構(gòu)造函數(shù)是私有的
private static Object obj = new Object();
public static Singleton getInstance() { // 通過靜態(tài)方法來構(gòu)造對(duì)象
if (instance == null) { // 關(guān)鍵點(diǎn) 2:判斷單例對(duì)象是否已經(jīng)被構(gòu)造
lock(obj) { // 關(guān)鍵點(diǎn) 3:加線程鎖
if (instance == null) { // 關(guān)鍵點(diǎn) 4:二次判斷單例是否已經(jīng)被構(gòu)造
instance = new Singleton();
}
}
}
return instance;
}
}
這個(gè)版本看出優(yōu)秀在哪里了嗎?
- 懶加載
- 確保線程安全
- 只有第一次創(chuàng)建類的時(shí)候可能發(fā)生阻塞,后面由于非空判斷都不會(huì)阻塞
- volatile 用來保證多個(gè)線程并發(fā)時(shí),訪問的都是內(nèi)存中的同一個(gè) volatile 對(duì)象。
缺點(diǎn)就是仍然可以通過反射等方式產(chǎn)生多個(gè)對(duì)象!
5. 靜態(tài)內(nèi)部類/Holder模式
我們既希望利用餓漢模式中靜態(tài)變量的方便和線程安全;又希望通過懶加載規(guī)避資源浪費(fèi)。Holder 模式滿足了這兩點(diǎn)要求:核心仍然是靜態(tài)變量,足夠方便和線程安全;通過靜態(tài)的 Holder 類持有真正實(shí)例,間接實(shí)現(xiàn)了懶加載。
原理就是說,靜態(tài)內(nèi)部類會(huì)在第一次被使用的時(shí)候被初始化,并且也只會(huì)被初始化一次,所以也包含懶加載和線程安全的特性。
public class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
StaticSingleton 被加載時(shí),內(nèi)部類不會(huì)被實(shí)例化,確保 StaticSingleton 類被載入 jvm 時(shí),不會(huì)被初始化單例類,而當(dāng) getInstance() 方法被調(diào)用時(shí),才加載 SingletonHolder,從而初始化 instance。同時(shí)用于實(shí)例的建立在類加載時(shí)完成,故天生對(duì)線程友好。
使用內(nèi)部類完成單利模式,既可以做到延遲加載,也不用使用同步關(guān)鍵字,是一種比較完善的做法。
這種寫法仍然使用 JVM 本身機(jī)制保證了線程安全問題;由于 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;同時(shí)讀取實(shí)例的時(shí)候不會(huì)進(jìn)行同步,沒有性能缺陷;也不依賴 JDK 版本。
6. 枚舉單例
用枚舉實(shí)現(xiàn)單例模式,相當(dāng)好用,但可讀性是不存在的。
枚舉類型也是在第一次被使用的時(shí)候初始化,并且默認(rèn)構(gòu)造函數(shù)是 private 修飾,而且線程安全。
以上的單例還是在運(yùn)用各種技巧來實(shí)現(xiàn),最后一種簡(jiǎn)直是利用規(guī)則來實(shí)現(xiàn)。這種代碼也及其簡(jiǎn)潔,只需要三行就可以實(shí)現(xiàn),但是可惜的是在面試中并不適用,因?yàn)楹芏嗝嬖嚬倏赡芤膊⒉涣私膺@個(gè)特性,那就是枚舉類。Java 的枚舉類是天生單例的,并且能夠?qū)Χ嗑€程免疫,對(duì)序列化免疫,簡(jiǎn)直是神器。
// 將枚舉的靜態(tài)成員變量作為單例的實(shí)例
public enum Singleton {
INSTANCE;
}
面試問題
單例模式實(shí)現(xiàn)的關(guān)鍵點(diǎn)
- 私有構(gòu)造函數(shù)(private):不能由其他類任意 new 單例模式的類
-
getInstance()是靜態(tài)方法(static):因?yàn)橛昧?code>類名.getInstance()來調(diào)用方法 - 聲明靜態(tài)單例對(duì)象:因?yàn)?code>getInstance()方法是static的,由于靜態(tài)方法只能訪問靜態(tài)變量,所以單例對(duì)象instance也必須是static的,而且靜態(tài)變量只會(huì)被初始化一次
- 構(gòu)造單例對(duì)象之前要加鎖(lock 一個(gè)靜態(tài)的 object 對(duì)象)
- 需要兩次檢測(cè)單例實(shí)例是否已經(jīng)被構(gòu)造,分別在鎖之前和鎖之后
為何要檢測(cè)兩次?
有可能延遲加載或者緩存原因,造成構(gòu)造多個(gè)實(shí)例,違反了單例的初衷。
構(gòu)造函數(shù)能否公有化?
不行,單例類的構(gòu)造函數(shù)必須私有化,單例類不能被實(shí)例化,單例實(shí)例只能靜態(tài)調(diào)用
lock 住的對(duì)象為什么要是 object 對(duì)象,可以是 int 嗎?
不行,鎖住的必須是個(gè)引用類型。如果鎖值類型,每個(gè)不同的線程在聲明的時(shí)候值類型變量的地址都不一樣,那么上個(gè)線程鎖住的東西下個(gè)線程進(jìn)來會(huì)認(rèn)為根本沒鎖,相當(dāng)于每次都鎖了不同的門,沒有任何卵用。而引用類型的變量地址是相同的,每個(gè)線程進(jìn)來判斷鎖多想是否被鎖的時(shí)候都是判斷同一個(gè)地址,相當(dāng)于是鎖在通一扇門,起到了鎖的作用。
如何選擇各種實(shí)現(xiàn)方式
俗話說,No silver bullet,每一種實(shí)現(xiàn)都有其適用的場(chǎng)景。那么,我們?nèi)绾芜x擇單例的實(shí)現(xiàn)方式呢?答案是:取決于你所期望的內(nèi)容。
如果你的單例類應(yīng)用頻繁,從系統(tǒng)啟動(dòng)后就需要使用,那么,餓漢式可能是一個(gè)不錯(cuò)的選擇。類加載過程便已經(jīng)完成了實(shí)例化的單例,在之后的調(diào)用過程中,無需再進(jìn)行實(shí)例化,也無需害怕因?yàn)榫€程同步導(dǎo)致的性能損耗。
如果你的單例類占用較多資源,并且調(diào)用頻率較低,那么或許 Double-Check 的懶漢式是一個(gè)不錯(cuò)的選擇。在單例使用前,并不會(huì)被實(shí)例化,其所需要的資源也并不會(huì)被占用。
如果你的單例類屬于某一個(gè)類庫(kù),或許 Double-Check 的懶漢式是一個(gè)不錯(cuò)的選擇。一個(gè)功能豐富的類庫(kù)中,并非所有的類都會(huì)被使用。然而 ClassLoader 的加載機(jī)制,并不一定會(huì)將其排除至外。所以,一個(gè)懶漢式的單例有可能降低類庫(kù)使用者的資源損耗。
一般來說,如果項(xiàng)目中不需要針對(duì)多線程情況的話,懶漢式、餓漢式的寫法都適用;如果需要保證多線程并行使用推薦靜態(tài)內(nèi)部類和枚舉
懶漢式與惡漢式對(duì)比
懶漢式單例是典型的時(shí)間換空間,每次取值都要時(shí)間做判斷,判斷是否需要?jiǎng)?chuàng)建實(shí)例,當(dāng)然如果沒有外部取值就不會(huì)創(chuàng)建對(duì)象,節(jié)約內(nèi)存空間。
餓漢式單例是典型的空間換時(shí)間,類裝載時(shí)就初始化實(shí)例,不管有沒有訪問取值,不需要做判斷節(jié)約時(shí)間,如果一直沒有外部訪問取值就浪費(fèi)了內(nèi)存空間。
你知道懶加載嗎?是怎么用在單例創(chuàng)建上的?有什么優(yōu)勢(shì)?
如果某個(gè)實(shí)例的創(chuàng)建 (比如數(shù)據(jù)庫(kù)連接池的創(chuàng)建) 需要消耗很多系統(tǒng)資源,就需要引入懶加載機(jī)制。即上面的代碼在類加載時(shí)就創(chuàng)建好了,如果在程序中始終沒用到這個(gè)實(shí)例就會(huì)浪費(fèi)很多系統(tǒng)資源。
為避免這種情況,就引入了懶加載機(jī)制,即在使用這個(gè)實(shí)例的時(shí)候才創(chuàng)建它。
參考資料
設(shè)計(jì)模式干貨系列:(四)單例模式【學(xué)習(xí)難度:★☆☆☆☆,使用頻率:★★★★☆】
單例模式各版本的原理與實(shí)踐
【創(chuàng)建型模式四】單例模式(Singleton)
單例模式(詳解,面試問題)
如何正確地寫出單例模式
單例模式 - 如何簡(jiǎn)單的理解單例模式
Java 設(shè)計(jì)模式學(xué)習(xí)(一) - 單例模式
如何寫線程安全的單例模式
Java 設(shè)計(jì)模式之單例模式
設(shè)計(jì)模式(三)——JDK 中的那些單例