1. 程式人生 > >java基礎總結(二十七)-- 單例模式的建立方式之一雙檢索,有什麼缺陷嗎?

java基礎總結(二十七)-- 單例模式的建立方式之一雙檢索,有什麼缺陷嗎?

來自:https://blog.csdn.net/a_842297171/article/details/79316591

 

這幾天看併發程式設計的書,發現原先寫的單例模式有點問題,當時認為雙重檢查是安全的,現在有新的瞭解。下面是雙重檢查寫法:

public static LasyModeSingletonVersion4 getSole() {
        //只有sole為空時才構造,否則直接返回
        if(null == sole) {
            //在構造的臨界區上加鎖,而不是整個方法加鎖
            synchronized(LasyModeSingletonVersion4.class) {
                //獲得鎖之後繼續再一次判斷,這樣就安全了
                if(null == sole) {
                    sole = new LasyModeSingletonVersion4();
                }

            }
        }
        return sole; 
    }

•乍看之下,發現挺完美:一個執行緒獲得同步鎖之後在還沒構造完成物件之前,其他執行緒永遠只能阻塞在構造語句之外,且還有一層雙重保險:再加一次校驗。 
•直到看到指令重排序,上面的程式我都認為是很安全的。指令重排序是JVM對語句執行的優化,只要語句間沒有依賴,那JVM就有權對語句進行優化。比如:

int a = 1;//①
int b = 1;//②
int c = a + b;//③

很簡單的a+b,我們寫程式碼在腦中構想或者進行斷點除錯都是這樣一個語句一個語句執行的,但是jvm在執行的時候卻不一定按這個順序。在這裡要說下as-is-serial這個東西,as-if-serial是指在執行結果不會改變的情況下,JVM為了提高程式的執行效率會對指令進行重排序。就像上面的程式,為了保證程式的正確性,③是一定要在①和②之後執行的,但是①和②則就沒有依賴了,①和②誰先執行都不影響結果,所以JVM就可能會對它們進行重排序。as-if-serial保證了單執行緒下程式執行結果的正確性。 
•再看看雙重檢查的程式碼,synchronized塊裡面的程式碼我們可以看成是單執行緒程式,因為同一時刻只有一個執行緒在執行,裡面的語句很簡單,就是判斷是否為空,如果為空則構建物件,現在需要思考的是構建物件是否可能會被JVM重排序,很遺憾,sole = new LasyModeSingletonVersion4();這條語句並沒有原子性,就像i++;一樣,它是被分為好幾步完成的:
 

①分配空間給物件
②在空間內建立物件
③將物件賦值給引用sole

•上面的語句中,②是依賴於①的,所以②在①之後執行,但是③和②不存在依賴性,也就是執行順序可能是:①->③->②,如果是單執行緒的程式(真的只有一個執行緒可以訪問到它們),那麼如果後續程式使用到了sole,JVM會保證你使用sole的時候是初始化完成的,但是現在在synchronized塊之外有其它執行緒“虎視眈眈”,獲取到鎖的執行緒如果按照①->③->②的順序執行,那在執行③的時候會store-write,即將值寫回主記憶體,則其它執行緒會讀到最新的sole值,而現在這個sole指向的是一個不完全的物件,即不安全物件,也不可用,使用這個物件是有危險的,此時構造物件的執行緒還沒有釋放鎖,其它執行緒進行第一次檢查的時候,null == sole的結果是false,會返回這個物件,造成程式的異常。 
•一說到指令重排序,我們很容易想到volatile關鍵字,volatile禁止指令重排序,所以如果sole被volatile修飾的話,可以保證這個初始化的有序性。

•但是今天想介紹的是一個更好的單例模式,它是“天生”執行緒安全的:
 

class BetterSingleton{
    static{
        sole = new BetterSingleton();
    }

    public static BetterSingleton sole;
    private BetterSingleton() {
        System.out.println(new Random().nextInt(100000));
    }
    public static BetterSingleton getSole() {
        return sole;
    }
}

•看上去很簡單,將構建物件放在了static塊中,這裡就要說到< clinit >方法,它會在JVM初始化類時呼叫,包括靜態變數初始化語句和靜態塊的執行,而虛擬機器會保證類只保證初始化一次,所以類的初始化是單執行緒執行的,所以將單例例項化放在static塊是天生執行緒安全的。多執行緒環境下呼叫getSole()方法時,第一個呼叫的執行緒會引起類的初始化。所以這個單例是目前比較好的,當然還有列舉的單例。
--------------------- 
作者:么零小柒 
來源:CSDN 
原文:https://blog.csdn.net/a_842297171/article/details/79316591 
版權宣告:本文為博主原創文章,轉載請附上博文連結!