java程式中雙重檢查鎖定與延遲初始化

來源:互聯網
上載者:User

在java程式中,有時候可能需要延遲一些高開銷的對象初始化操作,並且只有在使用這些對象時才進行初始化。此時程式員可能會採用延遲初始化。但要正確實現安全執行緒的延遲初始化需要一些技巧,否則很容易出現問題。比如,下面是非安全執行緒的延遲初始化對象的範例程式碼:

public class UnsafeLazyInitialization {private static Instance instance;public static Instance getInstance() {if (instance == null) //1:A線程執行instance = new Instance(); //2:B線程執行return instance;}}

在UnsafeLazyInitialization中,假設A線程執行代碼1的同時,B線程執行代碼2。此時,線程A可能會看到instance引用的對象還沒有完成初始化(出現這種情況的原因見後文的“問題的根源”)。

對於UnsafeLazyInitialization,我們可以對getInstance()做同步處理來實現安全執行緒的延遲初始化。範例程式碼如下:

public class SafeLazyInitialization {    private static Instance instance;    public synchronized static Instance getInstance() {        if (instance == null)            instance = new Instance();        return instance;    }}

由於對getInstance()做了同步處理,synchronized將導致效能開銷。如果getInstance()被多個線程頻繁的調用,將會導致程式執行效能的下降。反之,如果getInstance()不會被多個線程頻繁的調用,那麼這個延遲初始化方案將能提供令人滿意的效能。

在早期的JVM中,synchronized(甚至是無競爭的synchronized)存在這巨大的效能開銷。因此,人們想出了一個“聰明”的技巧:雙重檢查鎖定(double-checked locking)。人們想通過雙重檢查鎖定來降低同步的開銷。下面是使用雙重檢查鎖定來實現延遲初始化的範例程式碼:

public class DoubleCheckedLocking {                 //1    private static Instance instance;                    //2    public static Instance getInstance() {               //3        if (instance == null) {                          //4:第一次檢查            synchronized (DoubleCheckedLocking.class) {  //5:加鎖                if (instance == null)                    //6:第二次檢查                    instance = new Instance();           //7:問題的根源出在這裡            }                                            //8        }                                                //9        return instance;                                 //10    }                                                    //11}                                                        //12

如上面代碼所示,如果第一次檢查instance不為null,那麼就不需要執行下面的加鎖和初始化操作。因此可以大幅降低synchronized帶來的效能開銷。上面代碼錶面上看起來,似乎兩全其美:

在多個線程試圖在同一時間建立對象時,會通過加鎖來保證只有一個線程能建立對象。

在對象建立好之後,執行getInstance()將不需要擷取鎖,直接返回已建立好的對象。

雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的最佳化!線上程執行到第4行代碼讀取到instance不為null時,instance引用的對象有可能還沒有完成初始化。

問題的根源

前面的雙重檢查鎖定範例程式碼的第7行(instance = new Singleton();)建立一個對象。這一行代碼可以分解為如下的三行虛擬碼:

memory = allocate();   //1:指派至的記憶體空間ctorInstance(memory);  //2:初始化對象instance = memory;     //3:設定instance指向剛分配的記憶體位址

上面三行虛擬碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的,詳情見參考文獻1的“Out-of-order writes”部分)。2和3之間重排序之後的執行時序如下:

memory = allocate();   //1:指派至的記憶體空間instance = memory;     //3:設定instance指向剛分配的記憶體位址                       //注意,此時對象還沒有被初始化!ctorInstance(memory);  //2:初始化對象

根據《The Java Language Specification, Java SE 7 Edition》(後文簡稱為java語言規範),所有線程在執行java程式時必須要遵守intra-thread semantics。intra-thread semantics保證重排序不會改變單線程內的程式執行結果。換句話來說,intra-thread semantics允許那些在單線程內,不會改變單線程程式執行結果的重排序。上面三行虛擬碼的2和3之間雖然被重排序了,但這個重排序並不會違反intra-thread semantics。這個重排序在沒有改變單線程程式的執行結果的前提下,可以提高程式的執行效能。

為了更好的理解intra-thread semantics,請看下面的示意圖(假設一個線程A在構造對象後,立即訪問這個對象):

如上圖所示,只要保證2排在4的前面,即使2和3之間重排序了,也不會違反intra-thread semantics。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.