欧美三区_成人在线免费观看视频_欧美极品少妇xxxxⅹ免费视频_a级毛片免费播放_鲁一鲁中文字幕久久_亚洲一级特黄

雙重檢查鎖定與延遲初始化

系統(tǒng) 1698 0

在java程序中,有時候可能需要推遲一些高開銷的對象初始化操作,并且只有在使用這些對象時才進行初始化。此時程序員可能會采用延遲初始化。但要正確實現(xiàn)線程安全的延遲初始化需要一些技巧,否則很容易出現(xiàn)問題。比如,下面是非線程安全的延遲初始化對象的示例代碼:

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

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

?

對于UnsafeLazyInitialization,我們可以對getInstance()做同步處理來實現(xiàn)線程安全的延遲初始化。示例代碼如下:

      public class SafeLazyInitialization {
    private static Instance instance;

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

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

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

      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,那么就不需要執(zhí)行下面的加鎖和初始化操作。因此可以大幅降低synchronized帶來的性能開銷。上面代碼表面上看起來,似乎兩全其美:

  • 在多個線程試圖在同一時間創(chuàng)建對象時,會通過加鎖來保證只有一個線程能創(chuàng)建對象。
  • 在對象創(chuàng)建好之后,執(zhí)行getInstance()將不需要獲取鎖,直接返回已創(chuàng)建好的對象。

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

問題的根源

前面的雙重檢查鎖定示例代碼的第7行(instance = new Singleton();)創(chuàng)建一個對象。這一行代碼可以分解為如下的三行偽代碼:

      memory = allocate();   //1:分配對象的內(nèi)存空間
ctorInstance(memory);  //2:初始化對象
instance = memory;     //3:設置instance指向剛分配的內(nèi)存地址
    

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

      memory = allocate();   //1:分配對象的內(nèi)存空間
instance = memory;     //3:設置instance指向剛分配的內(nèi)存地址
                       //注意,此時對象還沒有被初始化!
ctorInstance(memory);  //2:初始化對象
    

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

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

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

下面,再讓我們看看多線程并發(fā)執(zhí)行的時候的情況。請看下面的示意圖:

由于單線程內(nèi)要遵守intra-thread semantics,從而能保證A線程的程序執(zhí)行結果不會被改變。但是當線程A和B按上圖的時序執(zhí)行時,B線程將看到一個還沒有被初始化的對象。

※注:本文統(tǒng)一用紅色的虛箭線標識錯誤的讀操作,用綠色的虛箭線標識正確的讀操作。

回到本文的主題,DoubleCheckedLocking示例代碼的第7行(instance = new Singleton();)如果發(fā)生重排序,另一個并發(fā)執(zhí)行的線程B就有可能在第4行判斷instance不為null。線程B接下來將訪問instance所引用的對象,但此時這個對象可能還沒有被A線程初始化!下面是這個場景的具體執(zhí)行時序:

時間

線程A

線程B

t1

A1:分配對象的內(nèi)存空間

?

t2

A3:設置instance指向內(nèi)存空間

?

t3

?

B1:判斷instance是否為空

t4

?

B2:由于instance不為null,線程B將訪問instance引用的對象

t5

A2:初始化對象

?

t6

A4:訪問instance引用的對象

?

?

這里A2和A3雖然重排序了,但java內(nèi)存模型的intra-thread semantics將確保A2一定會排在A4前面執(zhí)行。因此線程A的intra-thread semantics沒有改變。但A2和A3的重排序,將導致線程B在B1處判斷出instance不為空,線程B接下來將訪問instance引用的對象。此時,線程B將會訪問到一個還未初始化的對象。

在知曉了問題發(fā)生的根源之后,我們可以想出兩個辦法來實現(xiàn)線程安全的延遲初始化:

  1. 不允許2和3重排序;
  2. 允許2和3重排序,但不允許其他線程“看到”這個重排序。

后文介紹的兩個解決方案,分別對應于上面這兩點。

基于volatile的雙重檢查鎖定的解決方案

對于前面的基于雙重檢查鎖定來實現(xiàn)延遲初始化的方案(指DoubleCheckedLocking示例代碼),我們只需要做一點小的修改(把instance聲明為volatile型),就可以實現(xiàn)線程安全的延遲初始化。請看下面的示例代碼:

      public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance();//instance為volatile,現(xiàn)在沒問題了
            }
        }
        return instance;
    }
}
    

注意,這個解決方案需要JDK5或更高版本(因為從JDK5開始使用新的JSR-133內(nèi)存模型規(guī)范,這個規(guī)范增強了volatile的語義)。

當聲明對象的引用為volatile后,“問題的根源”的三行偽代碼中的2和3之間的重排序,在多線程環(huán)境中將會被禁止。上面示例代碼將按如下的時序執(zhí)行:

這個方案本質(zhì)上是通過禁止上圖中的2和3之間的重排序,來保證線程安全的延遲初始化。

基于類初始化的解決方案

JVM在類的初始化階段(即在Class被加載后,且被線程使用之前),會執(zhí)行類的初始化。在執(zhí)行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。

基于這個特性,可以實現(xiàn)另一種線程安全的延遲初始化方案(這個方案被稱之為Initialization On Demand Holder idiom):

      public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }

    public static Instance getInstance() {
        return InstanceHolder.instance ;  //這里將導致InstanceHolder類被初始化
    }
}
    

假設兩個線程并發(fā)執(zhí)行getInstance(),下面是執(zhí)行的示意圖:

這個方案的實質(zhì)是:允許“問題的根源”的三行偽代碼中的2和3重排序,但不允許非構造線程(這里指線程B)“看到”這個重排序。

初始化一個類,包括執(zhí)行這個類的靜態(tài)初始化和初始化在這個類中聲明的靜態(tài)字段。根據(jù)java語言規(guī)范,在首次發(fā)生下列任意一種情況時,一個類或接口類型T將被立即初始化:

  • T是一個類,而且一個T類型的實例被創(chuàng)建;
  • T是一個類,且T中聲明的一個靜態(tài)方法被調(diào)用;
  • T中聲明的一個靜態(tài)字段被賦值;
  • T中聲明的一個靜態(tài)字段被使用,而且這個字段不是一個常量字段;
  • T是一個頂級類(top level class,見java語言規(guī)范的§7.6),而且一個斷言語句嵌套在T內(nèi)部被執(zhí)行。

在InstanceFactory示例代碼中,首次執(zhí)行getInstance()的線程將導致InstanceHolder類被初始化(符合情況4)。

由于java語言是多線程的,多個線程可能在同一時間嘗試去初始化同一個類或接口(比如這里多個線程可能在同一時刻調(diào)用getInstance()來初始化InstanceHolder類)。因此在java中初始化一個類或者接口時,需要做細致的同步處理。

Java語言規(guī)范規(guī)定,對于每一個類或接口C,都有一個唯一的初始化鎖LC與之對應。從C到LC的映射,由JVM的具體實現(xiàn)去自由實現(xiàn)。JVM在類初始化期間會獲取這個初始化鎖,并且每個線程至少獲取一次鎖來確保這個類已經(jīng)被初始化過了(事實上,java語言規(guī)范允許JVM的具體實現(xiàn)在這里做一些優(yōu)化,見后文的說明)。

對于類或接口的初始化,java語言規(guī)范制定了精巧而復雜的類初始化處理過程。java初始化一個類或接口的處理過程如下(這里對類初始化處理過程的說明,省略了與本文無關的部分;同時為了更好的說明類初始化過程中的同步處理機制,筆者人為的把類初始化的處理過程分為了五個階段):

第一階段:通過在Class對象上同步(即獲取Class對象的初始化鎖),來控制類或接口的初始化。這個獲取鎖的線程會一直等待,直到當前線程能夠獲取到這個初始化鎖。

假設Class對象當前還沒有被初始化(初始化狀態(tài)state此時被標記為state = noInitialization),且有兩個線程A和B試圖同時初始化這個Class對象。下面是對應的示意圖:

下面是這個示意圖的說明:

時間

線程A

線程B

t1

A1:嘗試獲取Class對象的初始化鎖。這里假設線程A獲取到了初始化鎖

B1:嘗試獲取Class對象的初始化鎖,由于線程A獲取到了鎖,線程B將一直等待獲取初始化鎖

t2

A2:線程A看到線程還未被初始化(因為讀取到state == noInitialization),線程設置state = initializing

?

t3

A3:線程A釋放初始化鎖

?

?

第二階段:線程A執(zhí)行類的初始化,同時線程B在初始化鎖對應的condition上等待:

下面是這個示意圖的說明:

時間

線程A

線程B

t1

A1:執(zhí)行類的靜態(tài)初始化和初始化類中聲明的靜態(tài)字段

B1:獲取到初始化鎖

t2

?

B2:讀取到state == initializing

t3

?

B3:釋放初始化鎖

t4

?

B4:在初始化鎖的condition中等待

?

第三階段:線程A設置state = initialized,然后喚醒在condition中等待的所有線程:

下面是這個示意圖的說明:

時間

線程A

t1

A1:獲取初始化鎖

t2

A2:設置state = initialized

t3

A3:喚醒在condition中等待的所有線程

t4

A4:釋放初始化鎖

t5

A5:線程A的初始化處理過程完成

?

第四階段:線程B結束類的初始化處理:

下面是這個示意圖的說明:

時間

線程B

t1

B1:獲取初始化鎖

t2

B2:讀取到state == initialized

t3

B3:釋放初始化鎖

t4

B4:線程B的類初始化處理過程完成

?

線程A在第二階段的A1執(zhí)行類的初始化,并在第三階段的A4釋放初始化鎖;線程B在第四階段的B1獲取同一個初始化鎖,并在第四階段的B4之后才開始訪問這個類。根據(jù)java內(nèi)存模型規(guī)范的鎖規(guī)則,這里將存在如下的happens-before關系:

這個happens-before關系將保證:線程A執(zhí)行類的初始化時的寫入操作(執(zhí)行類的靜態(tài)初始化和初始化類中聲明的靜態(tài)字段),線程B一定能看到。

第五階段:線程C執(zhí)行類的初始化的處理:

下面是這個示意圖的說明:

時間

線程B

t1

C1:獲取初始化鎖

t2

C2:讀取到state == initialized

t3

C3:釋放初始化鎖

t4

C4:線程C的類初始化處理過程完成

?

在第三階段之后,類已經(jīng)完成了初始化。因此線程C在第五階段的類初始化處理過程相對簡單一些(前面的線程A和B的類初始化處理過程都經(jīng)歷了兩次鎖獲取-鎖釋放,而線程C的類初始化處理只需要經(jīng)歷一次鎖獲取-鎖釋放)。

線程A在第二階段的A1執(zhí)行類的初始化,并在第三階段的A4釋放鎖;線程C在第五階段的C1獲取同一個鎖,并在在第五階段的C4之后才開始訪問這個類。根據(jù)java內(nèi)存模型規(guī)范的鎖規(guī)則,這里將存在如下的happens-before關系:

這個happens-before關系將保證:線程A執(zhí)行類的初始化時的寫入操作,線程C一定能看到。

※注1:這里的condition和state標記是本文虛構出來的。Java語言規(guī)范并沒有硬性規(guī)定一定要使用condition和state標記。JVM的具體實現(xiàn)只要實現(xiàn)類似功能即可。

※注2:Java語言規(guī)范允許Java的具體實現(xiàn),優(yōu)化類的初始化處理過程(對這里的第五階段做優(yōu)化),具體細節(jié)參見java語言規(guī)范的12.4.2章。

通過對比基于volatile的雙重檢查鎖定的方案和基于類初始化的方案,我們會發(fā)現(xiàn)基于類初始化的方案的實現(xiàn)代碼更簡潔。但基于volatile的雙重檢查鎖定的方案有一個額外的優(yōu)勢:除了可以對靜態(tài)字段實現(xiàn)延遲初始化外,還可以對實例字段實現(xiàn)延遲初始化。

總結

延遲初始化降低了初始化類或創(chuàng)建實例的開銷,但增加了訪問被延遲初始化的字段的開銷。在大多數(shù)時候,正常的初始化要優(yōu)于延遲初始化。如果確實需要對實例字段使用線程安全的延遲初始化,請使用上面介紹的基于volatile的延遲初始化的方案;如果確實需要對靜態(tài)字段使用線程安全的延遲初始化,請使用上面介紹的基于類初始化的方案。

雙重檢查鎖定與延遲初始化


更多文章、技術交流、商務合作、聯(lián)系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯(lián)系: 360901061

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦!!!

發(fā)表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 一区二区三区日韩精品 | 国产精品亚洲精品日韩已方 | 亚洲综合色视频在线观看 | 黄视频免费在线观看 | 日日操日日舔 | 亚洲成片在线观看12345ba | 特黄特色大片免费视频大全 | 999国产视频 | 国产精品美女久久久久久久久久久 | 一区二区三区在线 | 免费观看毛片 | 美味人妻2中文A片 | 精品久久久久久亚洲 | 日本精品在线播放 | 四虎影片国产精品8848 | 亚洲国产精品热久久2022 | 五月综合久久 | 日韩欧美大片在线观看 | 久久国产一区 | 亚洲欧美精品中字久久99 | 欧美91精品国产自产 | 日韩在线精品视频 | 成人久久18免费观看 | 欧美亚洲 尤物久久 综合精品 | 狠狠操狠狠干 | 欧美亚洲另类视频 | av黄色在线免费观看 | 狠狠狠狠狠狠狠狠狠狠 | 成人免费观看网欧美片 | 性色网站| 亚洲欧美在线观看一区二区 | 黄在线观看在线播放720p | 成人午夜视频网站 | 在线观看国产免费高清不卡 | 99久久99| 91久久久久久久一区二区 | 国产噜噜噜 | 国产黄三级三·级三级 | 亚洲六月丁香色婷婷综合久久 | 国产欧美亚洲精品a | 欧美日韩图区 |