java的單例模式,為什麼需要volatile及靜態內部類
目前看了java併發的書,記錄一下。對於java的單例模式,正確的程式碼應該為:
public class TestInstance { private volatile static TestInstance instance; public static TestInstance getInstance() { //1 if (instance == null) { //2 synchronized (TestInstance.class) {//3 if (instance == null) { //4 instance = new TestInstance();//5 } } } return instance;//6 } }
以前不瞭解為什麼需要volatile關鍵字,後來發現在併發情況下,如果沒有volatile關鍵字,在第5行會出現問題
對於第5行
instance = new TestInstance();
可以分解為3行虛擬碼
1 memory=allocate();// 分配記憶體 相當於c的malloc
2 ctorInstanc(memory) //初始化物件
3 instance=memory //設定instance指向剛分配的地址
上面的程式碼在編譯器執行時,可能會出現重排序 從1-2-3 排序為1-3-2
如此在多執行緒下就會出現問題
例如現在有2個執行緒A,B
執行緒A在執行第5行程式碼時,B執行緒進來,而此時A執行了 1和3,沒有執行2,此時B執行緒判斷instance不為null 直接返回一個未初始化的物件,就會出現問題
而用了volatile,上面的重排序就會在多執行緒環境中禁止,不會出現上述問題。
靜態內部類模式:
-
public class SingleTon{
-
private SingleTon(){}
-
private static class SingleTonHoler{
-
private static SingleTon INSTANCE = new SingleTon();
-
}
-
public static SingleTon getInstance(){
-
return SingleTonHoler.INSTANCE;
-
}
-
}
靜態內部類的優點是:外部類載入時並不需要立即載入內部類,內部類不被載入則不去初始化INSTANCE,故而不佔記憶體。即當SingleTon第一次被載入時,並不需要去載入SingleTonHoler,只有當getInstance()方法第一次被呼叫時,才會去初始化INSTANCE,第一次呼叫getInstance()方法會導致虛擬機器載入SingleTonHoler類,這種方法不僅能確保執行緒安全,也能保證單例的唯一性,同時也延遲了單例的例項化。
那麼,靜態內部類又是如何實現執行緒安全的呢?首先,我們先了解下類的載入時機。
類載入時機:JAVA虛擬機器在有且僅有的5種場景下會對類進行初始化。
1.遇到new、getstatic、setstatic或者invikestatic這4個位元組碼指令時,對應的java程式碼場景為:new一個關鍵字或者一個例項化物件時、讀取或設定一個靜態欄位時(final修飾、已在編譯期把結果放入常量池的除外)、呼叫一個類的靜態方法時。
2.使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒進行初始化,需要先呼叫其初始化方法進行初始化。
3.當初始化一個類時,如果其父類還未進行初始化,會先觸發其父類的初始化。
4.當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的類),虛擬機器會先初始化這個類。
5.當使用JDK 1.7等動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化。
這5種情況被稱為是類的主動引用,注意,這裡《虛擬機器規範》中使用的限定詞是"有且僅有",那麼,除此之外的所有引用類都不會對類進行初始化,稱為被動引用。靜態內部類就屬於被動引用的行列。
我們再回頭看下getInstance()方法,呼叫的是SingleTonHoler.INSTANCE,取的是SingleTonHoler裡的INSTANCE物件,跟上面那個DCL方法不同的是,getInstance()方法並沒有多次去new物件,故不管多少個執行緒去呼叫getInstance()方法,取的都是同一個INSTANCE物件,而不用去重新建立。當getInstance()方法被呼叫時,SingleTonHoler才在SingleTon的執行時常量池裡,把符號引用替換為直接引用,這時靜態物件INSTANCE也真正被建立,然後再被getInstance()方法返回出去,這點同餓漢模式。那麼INSTANCE在建立過程中又是如何保證執行緒安全的呢?在《深入理解JAVA虛擬機器》中,有這麼一句話:
虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確地加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個程序阻塞(需要注意的是,其他執行緒雖然會被阻塞,但如果執行<clinit>()方法後,其他執行緒喚醒之後不會再次進入<clinit>()方法。同一個載入器下,一個型別只會初始化一次。),在實際應用中,這種阻塞往往是很隱蔽的。
故而,可以看出INSTANCE在建立過程中是執行緒安全的,所以說靜態內部類形式的單例可保證執行緒安全,也能保證單例的唯一性,同時也延遲了單例的例項化。
那麼,是不是可以說靜態內部類單例就是最完美的單例模式了呢?其實不然,靜態內部類也有著一個致命的缺點,就是傳參的問題,由於是靜態內部類的形式去建立單例的,故外部無法傳遞引數進去,例如Context這種引數,所以,我們建立單例時,可以在靜態內部類與DCL模式裡自己斟酌。