單例模式是一種常用的設計模式,該模式提供了一種創建對象的方法,確保在程序中一個類最多只有一個實例。
單例有什么用處?
有一些對象其實我們只需要一個,比如線程池、緩存、對話框、處理偏好設置和注冊表的對象、日志對象,充當打印機、顯示等設備的驅動程序對象。其實,這類對象只能有一個實例,如果制造出來多個實例,就會導致許多問題,如:程序的行為異常、資源使用過量,或者是不一致的結果。
Singleton通常用來代表那些本質上唯一的系統組件,比如窗口管理器或者文件系統。
在Java中實現單例模式,需要一個靜態變量、一個靜態方法和私有的構造器。
經典的單例模式實現
對於一個簡單的單例模式,可以這樣實現:
-
定義一個私有的靜態變量uniqueInstance;
-
定義私有的構造方法。這樣別處的代碼無法通過調用該類的構造函數來實例化該類的對象,只能通過該類提供的靜態方法來得到該類的唯一實例;
-
提供一個getInstance()方法,該方法中判斷是否已經存在該類的實例,如果存在直接返回,不存在則新建一個再返回。代碼如下:
public class Singleton{
private static Singleton uniqueInstance;//私有靜態變量
//私有的構造器。這樣別處的代碼無法通過調用該類的構造函數來實例化該類的對象,只能通過該類提供的靜態方法來得到該類的唯一實例。
private Singleton(){}
//靜態方法
public static Singleton getInstance(){
//如果不存在,利用私有構造器產生一個Singleton實例並賦值到uniqueInstance靜態變量中。
//如果我們不需要這個實例,他就永遠不會產生。這叫做“延遲實例化(懶加載)“
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
這段代碼使用了延遲實例化,在單線程中沒有任何問題。但是在多線程環境下,當有多個線程並行調用 getInstance(),都認為uniqueInstance為null的時候,就會調用uniqueInstance = new Singleton();
,這樣就會創建多個Singleton實例,無法保證單例。
解決多線程環境下的線程安全問題,主要有以下幾種寫法:
同步getInstance()方法
關鍵字synchronized可以保證在他同一時刻,只有一個線程可以執行某一個方法,或者某一個代碼塊。
同步getInstance()方法是處理多線程最直接的做法。只要把getInstance()變成同步(synchronized)方法,就可以解決並發問題了。
public class Singleton{
private static Singleton uniqueInstance;//私有靜態變量
//私有構造器
private Singleton() {}
//synchronized同步方法
public static synchronized Singleton getInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
但是,同步的效率低,會降低性能。只有第一次執行此方法的時候,才真正需要同步。也就是說,一旦設置好uniqueInstance變量,就不再需要同步這個方法了。之后每次調用這個方法,同步都是一種累贅。同步getInstance()方法既簡單又有效。如果說對性能要求不高,這樣就可以滿足要求。
“急切”實例化
之前的實現采用的是懶加載方式,也就是說,當真正用到的時候才會創建;如果沒被使用到,就一直不會創建。
懶加載方式在第一次使用的時候, 需要進行初始化操作,可能會比較耗時。
如果確定一個對象一定會使用的話,可以采用“急切”地實例化,事先准備好這個對象,需要的時候直接使用就行了。這種方式也叫做餓漢模式。具體代碼:
public class Singleton{
//在靜態初始化器中創建單例,保證了線程安全性
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance(){
return uniqueInstance;
}
}
餓漢模式是如何保證線程安全的?
餓漢模式中的靜態變量是隨着類加載時被初始化的。static關鍵字保證了該變量是類級別的,也就是說這個類被加載的時候被初始化一次。注意與對象級別和方法級別進行區分。
因為類的初始化是由類加載器完成的,這其實是利用了類加載器的線程安全機制。類加載器的loadClass方法在加載類的時候使用了synchronized關鍵字。也正是因為這樣, 除非被重寫,這個方法默認在整個裝載過程中都是同步的(線程安全的)。
雙重檢查加鎖
殺雞用牛刀。實現單例模式可以利用雙重檢查加鎖(double-checked locking),首先檢查是否實例已經創建了,如果尚未創建,“才”進行同步。這樣,只有第一次會同步。
public class Singleton{
//使用volatile關鍵字,確保當uniqueInstance變量被初始化成為Singleton實例時,多線程可以正確地處理uniqueInstance變量。
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if(uniqueInstance == null){//第一次檢查
synchronized(Singleton.class){
if(uniqueInstance == null){//第二次檢查
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
如果性能是關注的重點,雙重檢查加鎖可以大幅減少getInstance()的時間消耗成本。
在Java 1.5發行版本之前,雙重檢查模式的功能很不穩定,因為volatile修飾符的語義不夠強,難以支持它。Java 1.5發行版本中引入的內存模式解決了這個問題,如今,雙重檢查模式是延遲初始化的一個實例域的方法。
為什么要進行雙重檢查?只檢查一次不行嗎?
解答:只檢查一次不行。只檢查一次的代碼如下:
if(uniqueInstance == null){//第一次檢查
synchronized(Singleton.class){
uniqueInstance = new Singleton();
}
}
當兩個線程同時判斷uniqueInstance == null的時候,都會去獲得Singleton.class的鎖對象,由於兩個線程擁有的鎖對象是同一個Singleton.class,兩個線程先后執行,也就是兩個線程都會進入同步代碼塊創建一個新的對象,造成返回的uniqueInstance 並不是唯一的,這樣也就不符合單例模式了。
最佳方法
從Java 1.5發行版本起,實現Singleton只需要編寫一個包含單個元素的枚舉類型:
public enum Singleton {
INSTANCE;
}
使用枚舉實現單例的方法雖然還沒有廣泛采用,但是單元素的枚舉類型已經成為實現Singleton的最佳方法。注意:如果Singleton必須拓展一個超類,而不是擴展Enum的時候,則不宜使用這個方法。
參考
- Eric Freeman;ElElisabeth Freeman.HeadFirst設計模式[M]. 北京:中國電力出版社, 2007.
- Joshua Bloch.Effective Java中文版(原書第3版)[M]. 北京:機械工業出版社, 2018.
- 漫話:如何給女朋友解釋什么是單例模式?