引言
單例,顧名思義就是唯一一個單獨的存在.在實際開發(fā)過程中,某些對象會反復(fù)創(chuàng)建,為了減少同一個對象new次數(shù),減少系統(tǒng)內(nèi)存的使用頻率,減輕 GC 壓力.因此在使用周期內(nèi)只需要維持一個對象即可.
單例模式UML類圖
單例的特點
- 私有的構(gòu)造函數(shù)
- 使用該類的公有方法獲取該對象實例
- 該類在程序運行期間有且只有一個實例
最簡單的單例
/**
* 餓漢單例模式
*/
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton(){
System.out.print("HungrySingleton created\n");
}
public static HungrySingleton getInstance(){
return INSTANCE;
}
public static void doTest(){
System.out.print("doTest\n");
}
public static void main(String[] args) {
//驗證是否為單例
// HungrySingleton s1 = HungrySingleton.getInstance();
// HungrySingleton s2 = HungrySingleton.getInstance();
// System.out.printf(String.valueOf(s1 == s2));
HungrySingleton.doTest();
}
}
這種寫法可以達到單例的目的,保證了多線程下的線程安全問題,但是這種單例的寫法存在問題.在執(zhí)行HungrySingleton.doTest();
之后輸出的結(jié)果如下
HungrySingleton created
doTest
在HungrySingleton被加載時就會創(chuàng)建一個HungrySingleton的實例.無論getInstance()
是否被調(diào)用都會被初始化.這種單例模式也叫餓漢式單例模式.顯然只是調(diào)用doTest()方法的話完全不需要初始化INSTANCE
.所以我們可以將這種方式進行優(yōu)化,進而引入懶漢式單例模式.
懶漢單例模式
/**
* 懶漢單例模式
*/
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton(){}
public static synchronized LazySingleton getInstance(){
if(instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
這樣的寫法只有在需要獲取實例時才初始化instance
,優(yōu)化了LazySingleton加載的速度,同時加上了synchronized
關(guān)鍵字使得該寫法是線程安全的.但是這樣的寫法,在多線程下,即使instance已經(jīng)初始化成功之后,每次訪問getInstance
方法時,只有一個線程能進入該方法,其他線程都在等待,這樣就大大影響了程序的性能.所以我們還需要對這樣的懶漢式寫法進行改進,我們引入雙重檢驗鎖模式.
雙重檢驗鎖單例模式
雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。在回去實例對象時會有兩次檢查instance == null
,一次是在同步塊外,一次是在同步塊內(nèi)。先來看下代碼
/**
* 雙重檢驗鎖單例模式
*/
public class DCLSingleton {
private volatile static DCLSingleton instance = null;//(4)
private DCLSingleton(){}
public static DCLSingleton getInstance(){
if(instance == null){ //語句(1) 同步塊外
synchronized (DCLSingleton.class){
if(instance == null){ //語句(2) 同步塊內(nèi),為了防止產(chǎn)生多個instance實例
instance = new DCLSingleton();//語句(3)
}
}
}
return instance;
}
}
明明已經(jīng)使用了同步代碼塊來進行加鎖了,為什么還要檢驗兩次呢?要解釋這個問題之前,我們先要了解下在執(zhí)行語句(3)時發(fā)生了什么. 語句(3)并不是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情.
- 給 instance 分配內(nèi)存
- 調(diào)用 DCLSingleton 的構(gòu)造函數(shù)來初始化成員變量
- 將instance引用指向分配的內(nèi)存空間(執(zhí)行完這步 instance 不為 null )
這里涉及到JVM內(nèi)存模型中的重排序問題,在不存在依賴性的情況下,JVM可以進行重排序優(yōu)化.首先上述步驟1必須在步驟2和步驟3之前執(zhí)行,但是步驟2,和步驟3之間并不存在依賴關(guān)系,所以有可能執(zhí)行的順序是1->2->3
,也有可能是1->3->2
.在執(zhí)行順序為1->3->2
的情況下,如果線程1進入了同步塊中,并執(zhí)行到了語句(3)
中的1->3
,instance
已經(jīng)有指向的內(nèi)存空間,所以此時instance != null
.那么問題來了,剛好在此刻有線程2進入了getInstance()
方法,并且剛好執(zhí)行了語句(1)
,此時的if(instance == null)
返回的為false
,所以返回將一個并未完全完成實例化的instance
對象,那么此刻肯定會造成程序出現(xiàn)問題.所以我們引入了volatic
關(guān)鍵字,用來防止JVM進行重排序優(yōu)化(JDK1.5前volatic
關(guān)鍵字存在問題,在JDK1.5之后進行了修復(fù)).這樣使用volatic
關(guān)鍵字和語句(1)
就達到多線程下線程安全的目的.同時語句(1)
還有一個作用就是在instance
已經(jīng)初始化之后,就無需進入同步代碼塊進行等待操作,大大的提高了程序的效率.語句(2)
的作用就是在多個線程同時執(zhí)行了語句(1)
,并在同步代碼塊之外進行等待時,為了防止產(chǎn)生多個instance實例.
靜態(tài)內(nèi)部類單例模式
可能有些人覺得雙重檢驗鎖模式的代碼不夠優(yōu)雅,好吧,那就來種優(yōu)雅的.使用靜態(tài)內(nèi)部類來實現(xiàn)單例模式.不多說,先上代碼.
/**
* 靜態(tài)內(nèi)部類實現(xiàn)單例
*/
public class StaticInnerSingleton {
private StaticInnerSingleton() {
}
//靜態(tài)內(nèi)部類
private static class SingletonHolder {
private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
}
public StaticInnerSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
這樣實現(xiàn)的好處既保證了線程安全,又是懶漢式的,簡直無敵,個人比較推崇這種實現(xiàn)方式.畢竟雙重檢測鎖的寫法有些人并沒有完全理解,而且寫起來要考慮下邏輯也是有點麻煩.當(dāng)然如果是直接引用標(biāo)準(zhǔn)的雙重檢測鎖寫法的當(dāng)我沒說0.0
.
枚舉實現(xiàn)單例模式
最后講一種最少代碼量實現(xiàn)單例的方案.同時也是?Effective Java?中推薦的,使用枚舉(Enum).
public enum EnumSingleton {
INSTANCE;
public void doTest(){
System.out.printf("doTest in enum singleton");
}
public static void main(String[] args) {
EnumSingleton singleton = EnumSingleton.INSTANCE;
singleton.doTest();
}
}
創(chuàng)建枚舉默認就是線程安全的,而且使用枚舉還能防止反序列化導(dǎo)致重新創(chuàng)建新的對象,再看看代碼量,乖乖,簡直利器.但是用枚舉實現(xiàn)單例,我看到確實不是很多.有可能很多人對枚舉使用的不多,而且使用JDK1.5
之后出來的,可能都不太熟.
單例使用注意
除了枚舉實現(xiàn)的方案外,在Java處理序列化和反序列化時,會破壞單例的設(shè)計.因為反序列化得到的對象,和序列化之前的對象,不是同一個對象,它們的內(nèi)存地址不相同。所以,假設(shè)一個單例類實現(xiàn)了Serializable接口,反序列化時內(nèi)存里就會出現(xiàn)多個實例,這違背了當(dāng)初設(shè)計單例的初衷。但是Java的序列化機制提供了一個鉤子方法,即私有的readresolve()
方法,可以讓我們來控制反序列化時得到的對象。以雙重檢測鎖單例模式為例
/**
* 雙重檢驗鎖單例模式
*/
public class DCLSingleton implements Serializable{
private volatile static DCLSingleton instance = null;
private DCLSingleton(){}
public static DCLSingleton getInstance(){
if(instance == null){ //語句(1) 同步塊外
synchronized (DCLSingleton.class){
if(instance == null){ //語句(2)同步塊內(nèi),為了防止產(chǎn)生多個instance實例
instance = new DCLSingleton(); //語句(3)
}
}
}
return instance;
}
/**
* 反序列化時要做處理
* @return 返回原來的實例
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException {
return instance;
}
}
總結(jié)
一般嚴(yán)格的來說,實現(xiàn)單例的方案總共為5種,分別為餓漢,懶漢,雙重檢測鎖,靜態(tài)內(nèi)部類,枚舉方案實現(xiàn)單例.有的人說是有7種,其中有些在多線程下會出現(xiàn)線程同步問題,如果不考慮多線程的話,其實還有幾種不嚴(yán)格的寫法,這里就不提了,百度上肯定有的.從性能上來說,靜態(tài)內(nèi)部類和枚舉應(yīng)該是比較好的.當(dāng)然,如果從實際業(yè)務(wù)上來設(shè)計單例的話,還要考慮這個單例的功能如何,所以要具體問題具體分析.