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