單例模式的惰性加載
通常當我們設計一個單例類的時候,會在類的内部構造這個類(通過構造函數,或者在定義處直接建立),并對外提供一個static getInstance方法提供擷取該單例對象的途徑。例如:
public class Singleton
{
private static Singleton instance = new Singleton();
private Singleton(){
…
}
public static Singleton getInstance(){
return instance;
}
}
這樣的代碼缺點是:第一次加載類的時候會連帶着建立Singleton執行個體,這樣的結果與我們所期望的不同,因為建立執行個體的時候可能并不是我們需要這個執行個體的時候。同時如果這個Singleton執行個體的建立非常消耗系統資源,而應用始終都沒有使用Singleton執行個體,那麼建立Singleton消耗的系統資源就被白白浪費了。
為了避免這種情況,我們通常使用惰性加載的機制,也就是在使用的時候才去建立。以上代碼的惰性加載代碼如下:
public class Singleton{
private static Singleton instance = null;
private Singleton(){
…
}
public static Singleton getInstance(){
if (instance == null)
instance = new Singleton();
return instance;
}
}
這樣,當我們第一次調用Singleton.getInstance()的時候,這個單例才被建立,而以後再次調用的時候僅僅傳回這個單例就可以了。
但是 惰性加載在多線程中的問題:
這是如果兩個線程A和B同時執行了該方法,然後以如下方式執行:
1. A進入if判斷,此時foo為null,是以進入if内
2. B進入if判斷,此時A還沒有建立foo,是以foo也為null,是以B也進入if内
3. A建立了一個Foo并傳回
4. B也建立了一個Foo并傳回
此時問題出現了,我們的單例被建立了兩次,而這并不是我們所期望的。
是以可以使用Class鎖機制 給getInstance方法加上一個synchronize字首,這樣每次隻允許一個現成調用getInstance方法:
*********PS:1
public static synchronized Singleton getInstance(){
if (instance == null)
instance = new Singleton();
return instance;
}
這種解決辦法的确可以防止錯誤的出現,但是它卻很影響性能:每次調用getInstance方法的時候都必須獲得Singleton的鎖,而實際上,當單例執行個體被建立以後,其後的請求沒有必要再使用互斥機制了
曾經有人為了解決以上問題,提出了double-checked locking的解決方案
*********PS:2
public static Singleton getInstance(){
if (instance == null)
synchronized(instance){
if(instance == null)
instance = new Singleton();
}
return instance;
}
讓我們來看一下這個代碼是如何工作的:首先當一個線程送出請求後,會先檢查instance是否為null,如果不是則直接傳回其内容,這樣避免了進入synchronized塊所需要花費的資源。其次,即使第2節提到的情況發生了,兩個線程同時進入了第一個if判斷,那麼他們也必須按照順序執行synchronized塊中的代碼,第一個進入代碼塊的線程會建立一個新的Singleton執行個體,而後續的線程則因為無法通過if判斷,而不會建立多餘的執行個體。
上述描述似乎已經解決了我們面臨的所有問題,但實際上,從JVM的角度講,這些代碼仍然可能發生錯誤。
對于JVM而言,它執行的是一個個Java指令。在Java指令中建立對象和指派操作是分開進行的,
也就是說instance = new Singleton();語句是分兩步執行的。但是JVM并不保證這兩個操作的先後順序,也就是說有可能JVM會為新的Singleton執行個體配置設定空間,然後直接指派給instance成員,然後再去初始化這個Singleton執行個體。這樣就使出錯成為了可能,我們仍然以A、B兩個線程為例:
1.A、B線程同時進入了第一個if判斷
2.A首先進入synchronized塊,由于instance為null,是以它執行instance = new Singleton();
3.由于JVM内部的優化機制,JVM先畫出了一些配置設定給Singleton執行個體的空白記憶體,并指派給instance成員(注意此時JVM沒有開始初始化這個執行個體),然後A離開了synchronized塊。
4.B進入synchronized塊,由于instance此時不是null,是以它馬上離開了synchronized塊并将結果傳回給調用該方法的程式。
5.此時B線程打算使用Singleton執行個體,卻發現它沒有被初始化,于是錯誤發生了。
為了實作慢加載,并且不希望每次調用getInstance時都必須互斥執行,最好并且最友善的解決辦法如下:(通過内部類實作多線程環境中的單例模式)
public class Singleton{
private Singleton(){
…
}
private static class SingletonContainer{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonContainer.instance;
}
}
JVM内部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證instance隻被建立一次,并且會保證把指派給instance的記憶體初始化完畢,這樣我們就不用擔心上面的問題(PS:2)。此外該方法也隻會在第一次調用的時候使用互斥機制,這樣就解決了低效問題(PS:1)。最後instance是在第一次加載SingletonContainer類時被建立的,而SingletonContainer類則在調用getInstance方法的時候才會被加載,是以也實作了惰性加載。
版權聲明:本文為CSDN部落客「weixin_34250709」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。
原文連結:https://blog.csdn.net/weixin_34250709/article/details/92042169