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

用happen-before規則重新審視DCL

系統 1611 0

轉載自 ---- http://lifethinker.iteye.com/blog/260515

?

????? 編寫Java多線程程序一直以來都是一件十分困難的事,多線程程序的bug很難測試,DCL(Double Check Lock)就是一個典型,因此對多線程安全的理論分析就顯得十分重要,當然這決不是說對多線程程序的測試就是不必要的。傳統上,對多線程程序的分析是通過 分析操作之間可能的執行先后順序,然而程序執行順序十分復雜,它與硬件系統架構,編譯器,緩存以及虛擬機的實現都有著很大的關系。僅僅為了分析多線程程序 就需要了解這么多底層知識確實不值得,況且當年選擇學Java就是因為不用理會煩人的硬件和操作系統,這導致了許多Java程序員不愿也不能從理論上分析 多線程程序的正確性。雖然99%的Java程序員都知道DCL不對,但是如果讓他們回答一些問題,DCL為什么不對?有什么修正方法?這個修正方法是正確 的嗎?如果不正確,為什么不正確?對于此類問題,他們一臉茫然,或者回答也許吧,或者很自信但其實并沒有抓住根本。

?

幸好現在還有另一條路可走,我們只需要利用幾個基本的happen-before規則就能從理論上分析Java多線程程序的正確性,而且不需要涉及 到硬件和編譯器的知識。接下來的部分,我會首先說明一下happen-before規則,然后使用happen-before規則來分析DCL,最后我以 我自己的例子來說明DCL的問題其實很常見,只是因為對DCL的過度關注反而忽略其問題本身,當然其忽略是有原因的,因為很多人并不知道DCL的問題到底 出在哪里。

?

?

Happen-Before規則

?

我們一般說一個操作happen-before另一個操作,這到底是什么意思呢?當說操作A happen-before操作B時,我們其實是在說在發生操作B之前,操作A對內存施加的影響能夠被觀測到。所謂“對內存施加的影響”就是指對變量的寫 入,“被觀測到”指當讀取這個變量時能夠得到剛才寫入的值(如果中間沒有發生其它的寫入)。聽起來很繞口?這就對了,請你保持耐心,舉個例子來說明一下。 線程Ⅰ執行了操作A:x=3,線程Ⅱ執行了操作B:y=x。如果操作Ahappen-before操作B,線程Ⅱ在執行操作B之前就確定操作"x=3"被 執行了,它能夠確定,是因為如果這兩個操作之間沒有任何對x的寫入的話,它讀取x的值將得到3,這意味著線程Ⅱ執行操作B會寫入y的值為3。如果兩個操作 之間還有對x的寫入會怎樣呢?假設線程Ⅲ在操作A和B之間執行了操作C: x=5,并且操作C和操作B之前并沒有happen-before關系(后面我會說明時間上的先后并不一定導致happen-before關系)。這時線 程Ⅱ執行操作B會講到x的什么值呢?3還是5?答案是兩者皆有可能,這是因為happen-before關系 保證一定 能夠觀測到前一個操作施加的內存影響,只有時間上的先后關系而并沒有happen-before關系 可能但并不保證 能觀測前一個操作施加的內存影響。如果讀到了值3,我們就說讀到了“ 陳舊 ”的數據。正是多種可能性導致了多線程的不確定性和復雜性,但是要分析多線程的安全性,我們只能分析確定性部分,這就要求找出happen-before關系,這又得利用happen-before規則。

?

下面是我列出的三條非常重要的happen-before規則,利用它們可以確定兩個操作之間是否存在happen-before關系。

  1. 同一個線程中,書寫在前面的操作happen-before書寫在后面的操作。這條規則是說,在 單線程 中操作間happen-before關系完全是由源代碼的順序決定的,這里的前提“在同一個線程中”是很重要的,這條規則也稱為 單線程規則 。這個規則多少說得有些簡單了,考慮到控制結構和循環結構,書寫在后面的操作可能happen-before書寫在前面的操作,不過我想讀者應該明白我的意思。
  2. 對鎖的unlock操作happen-before后續的對同一個鎖的lock操作。這里的“后續”指的是時間上的先后關系,unlock操作發 生在退出同步塊之后,lock操作發生在進入同步塊之前。這是條最關鍵性的規則,線程安全性主要依賴于這條規則。但是僅僅是這條規則仍然不起任何作用,它 必須和下面這條規則聯合起來使用才顯得意義重大。這里關鍵條件是必須對“同一個鎖”的lock和unlock。
  3. 如果操作A happen-before操作B,操作B happen-before操作C,那么操作A happen-before操作C。這條規則也稱為傳遞規則。

?

現在暫時放下happen-before規則,先探討一下“一個操作在時間上先于另一個操作發生”和“一個操作happen-before另一個操 作之間”的關系。兩者有關聯卻并不相同。關聯部分在第2條happen-before規則中已經談到了,通常我們得假定一個時間上的先后順序然后據此得出 happen-before關系。不同部分體現在,首先, 一個操作在時間上先于另一個操作發生,并不意味著一個操作happen-before另一個操作 ??聪旅娴睦樱?

Java代碼 ? 收藏代碼
  1. public ? void ?setX( int ?x)?{??
  2. ?? this .x?=?x;??????????????? //?(1) ??
  3. }??
  4. ??
  5. public ? int ?getX()?{??
  6. ?? return ?x;????????????????? //?(2) ??
  7. }??

假設線程Ⅰ先執行setX方法,接著線程Ⅱ執行getX方法,在時間上線程Ⅰ的操作A:this.x = x先于線程Ⅱ的操作B:return x。但是操作A卻并不happen-before操作B,讓我們逐條檢查三條happen-before規則。第1條規則在這里不適用,因為這時兩個不同 的線程。第2條規則也不適用,因為這里沒有任何同步塊,也就沒有任何lock和unlock操作。第3條規則必須基于已經存在的happen- before關系,現在沒有得出任何happen-before關系,因此第三條規則對我們也任何幫助。通過檢查這三條規則,我們就可以得出,操作A和操 作B之間沒有happen-before關系。這意味著如果線程Ⅰ調用了setX(3),接著線程Ⅱ調用了getX(),其返回值可能不是3,盡管兩個操 作之間沒有任何其它操作對x進行寫入,它可能返回任何一個曾經存在的值或者默認值0。“任何曾經存在的值”需要做點解釋,假設在線程Ⅰ調用setX(3) 之前,還有別的線程或者就是線程Ⅰ還調用過setX(5), setX(8),那么x的曾經可能值為0, 5和8(這里假設setX是唯一能夠改變x的方法),其中0是整型的默認值,用在這個例子中,線程Ⅱ調用getX()的返回值可能為0, 3, 5和8,至于到底是哪個值是不確定的。

?

現在將兩個方法都設成同步的,也就是如下:

Java代碼 ? 收藏代碼
  1. public ? synchronized ? void ?setX( int ?x)?{??
  2. ?? this .x?=?x;??????????????? //?(1) ??
  3. }??
  4. ??
  5. public ? synchronized ? int ?getX()?{??
  6. ?? return ?x;????????????????? //?(2) ??
  7. }??

做同樣的假設,線程Ⅰ先執行setX方法,接著線程Ⅱ執行getX方法,這時就可以得出來,線程Ⅰ的操作A happen-before線程Ⅱ的操作B。下面我們來看如何根據happen-before規則來得到這個結論。由于操作A處于同步塊中,操作A之后必 須定要發生對this鎖的unlock操作,操作B也處于同步塊中,操作B之前必須要發生對this鎖的lock操作,根據假設unlock操作發生 lock操作之前,根據第2條happen-before規則,就得到unlock操作happen-before于lock操作;另外根據第1條 happen-before規則(單線程規則),操作A happen-before于unlock操作,lock操作happen-before于操作B;最后根據第3條happen-before規則(傳遞 規則),A -> unlock, unlock -> lock, lock -> B(這里我用->表示happen-before關系),有 A -> B,也就是說操作A happen-before操作B。這意味著如果線程Ⅰ調用了setX(3),緊接著線程Ⅱ調用了getX(),如果中間再沒有其它線程改變x的值,那么 其返回值必定是3。

?

如果將兩個方法的任何一個synchronized關鍵字去掉又會怎樣呢?這時能不能得到線程Ⅰ的操作A happen-before線程Ⅱ的操作B呢?答案是得不到。這里因為第二條happen-before規則的條件已經不成立了,這時因為要么只有線程Ⅰ 的unlock操作(如果去掉getX的synchronized),要么只有線程Ⅱ的lock操作(如果去掉setX的synchronized關鍵 字)。這里也告訴我們一個原則, 必須對同一個變量的 所有 讀寫同步,才能保證不讀取到陳舊的數據,僅僅同步讀或寫是不夠的 。

?

其次, 一個操作happen-before另一個操作 也并不意味著 一個操作在時間上先于另一個操作發生 ??聪旅娴睦樱?

Java代碼 ? 收藏代碼
  1. x?=? 3 ;??????( 1 )??
  2. y?=? 2 ;??????( 2 )??

同一個線程執行上面的兩個操作,操作A:x = 3和操作B:y = 2。根據單線程規則,操作A happen-before操作B,但是操作A卻不一定在時間上先于操作B發生,這是因為編譯器的重新排序等原因,操作B可能在時間上后于操作B發生。這 個例子也說明了,分析操作上先后順序是多么地不靠譜,它可能完全違反直觀感覺。

?

最后,一個操作和另一個操作必定存在某個順序,要么一個操作或者是先于或者是后于另一個操作,或者與兩個操作同時發生。同時發生是完全可能存在的, 特別是在多CPU的情況下。而兩個操作之間卻可能沒有happen-before關系,也就是說有可能發生這樣的情況,操作A不happen- before操作B,操作B也不happen-before操作A,用數學上的術語happen-before關系是個偏序關系。兩個存在happen- before關系的操作不可能同時發生,一個操作A happen-before操作B,它們必定在時間上是完全錯開的,這實際上也是同步的語義之一(獨占訪問)。

?

在運用happen-before規則分析DCL之前,有必要對“操作”澄清一下,在前面的敘述中我一直將語句是操作的同義詞,這么講是不嚴格的, 嚴格上來說這里的操作應該是指單個虛擬機的指令,如moniterenter, moniterexit, add, sub, store, load等。使用語句來代表操作并不影響我們的分析,下面我仍將延續這一傳統,并且將直接用語句來代替操作。唯一需要注意的是單個語句實際上可能由多個指 令組成,比如語句x=i++由兩條指令(inc和store)組成?,F在我們已經完成了一切理論準備,你一定等不及要動手開干了(我都寫煩了)。

?

?

利用Happen-Before規則分析DCL

?

下面是一個典型的使用DCL的例子:

?

Java代碼 ? 收藏代碼
  1. public ? class ?LazySingleton?{??
  2. ???? private ? int ?someField;??
  3. ??????
  4. ???? private ? static ?LazySingleton?instance;??
  5. ??????
  6. ???? private ?LazySingleton()?{??
  7. ???????? this .someField?=? new ?Random().nextInt( 200 )+ 1 ;????????? //?(1) ??
  8. ????}??
  9. ??????
  10. ???? public ? static ?LazySingleton?getInstance()?{??
  11. ???????? if ?(instance?==? null )?{??????????????????????????????? //?(2) ??
  12. ???????????? synchronized (LazySingleton. class )?{??????????????? //?(3) ??
  13. ???????????????? if ?(instance?==? null )?{??????????????????????? //?(4) ??
  14. ????????????????????instance?=? new ?LazySingleton();??????????? //?(5) ??
  15. ????????????????}??
  16. ????????????}??
  17. ????????}??
  18. ???????? return ?instance;?????????????????????????????????????? //?(6) ??
  19. ????}??
  20. ??????
  21. ???? public ? int ?getSomeField()?{??
  22. ???????? return ? this .someField;???????????????????????????????? //?(7) ??
  23. ????}??
  24. }??

?

為了分析DCL,我需要預先陳述上面程序運行時幾個事實:

  1. 語句(5)只會被執行一次,也就是LazySingleton只會存在一個實例,這是由于它和語句(4)被放在同步塊中被執行的緣故,如果去掉語句(3)處的同步塊,那么這個假設便不成立了。
  2. instance只有兩種“曾經可能存在”的值,要么為null,也就是初始值,要么為執行語句(5)時構造的對象引用。這個結論由事實1很容易推出來。
  3. getInstance()總是返回非空值,并且每次調用返回相同的引用。如果getInstance()是初次調用,它會執行語句(5)構造一 個LazySingleton實例并返回,如果getInstance()不是初次調用,如果不能在語句(2)處檢測到非空值,那么必定將在語句(4)處 就能檢測到instance的非空值,因為語句(4)處于同步塊中,對instance的寫入--語句(5)也處于同一個同步塊中。

有讀者可能要問了,既然根據第3條事實getInstance()總是返回相同的正確的引用,為什么還說DCL有問題呢? 這里的關鍵是 盡管得到了LazySingleton的正確引用,但是卻有可能訪問到其成員變量 不正確值 ,具體來說LazySingleton.getInstance().getSomeField()有可能返回someField的默認值0。如果程序行 為正確的話,這應當是不可能發生的事,因為在構造函數里設置的someField的值不可能為0。為也說明這種情況理論上有可能發生,我們只需要說明語句 (1)和語句(7)并不存在happen-before關系。

?

假設線程Ⅰ是初次調用getInstance()方法,緊接著線程Ⅱ也調用了getInstance()方法和getSomeField()方法, 我們要說明的是線程Ⅰ的語句(1)并不happen-before線程Ⅱ的語句(7)。線程Ⅱ在執行getInstance()方法的語句(2)時,由于 對instance的訪問并沒有處于同步塊中,因此線程Ⅱ可能觀察到也可能觀察不到線程Ⅰ在語句(5)時對instance的寫入,也就是說 instance的值可能為空也可能為非空。我們先假設instance的值非空,也就觀察到了線程Ⅰ對instance的寫入,這時線程Ⅱ就會執行語句 (6)直接返回這個instance的值,然后對這個instance調用getSomeField()方法,該方法也是在沒有任何同步情況被調用,因此 整個線程Ⅱ的操作都是在沒有同步的情況下調用 ,這時我們無法利用第1條和第2條happen-before規則得到線程Ⅰ的操作和線程Ⅱ的操作之間的任何有效的happen-before關系,這說 明線程Ⅰ的語句(1)和線程Ⅱ的語句(7)之間并不存在happen-before關系,這就意味著線程Ⅱ在執行語句(7)完全有可能觀測不到線程Ⅰ在語 句(1)處對someFiled寫入的值,這就是DCL的問題所在。很荒謬,是吧?DCL原本是為了逃避同步,它達到了這個目的,也正是因為如此,它最終 受到懲罰,這樣的程序存在嚴重的bug,雖然這種bug被發現的概率絕對比中彩票的概率還要低得多,而且是轉瞬即逝,更可怕的是,即使發生了你也不會想到 是DCL所引起的。

?

前面我們說了,線程Ⅱ在執行語句(2)時也有可能觀察空值,如果是種情況,那么它需要進入同步塊,并執行語句(4)。在語句(4)處線程Ⅱ還能夠讀 到instance的空值嗎?不可能。這里因為這時對instance的寫和讀都是發生在同一個鎖確定的同步塊中,這時讀到的數據是最新的數據。為也加深 印象,我再用happen-before規則分析一遍。線程Ⅱ在語句(3)處會執行一個lock操作,而線程Ⅰ在語句(5)后會執行一個unlock操 作,這兩個操作都是針對同一個鎖--LazySingleton.class,因此根據第2條happen-before規則,線程Ⅰ的unlock操作 happen-before線程Ⅱ的lock操作,再利用單線程規則,線程Ⅰ的語句(5) -> 線程Ⅰ的unlock操作,線程Ⅱ的lock操作 -> 線程Ⅱ的語句(4),再根據傳遞規則,就有線程Ⅰ的語句(5) -> 線程Ⅱ的語句(4),也就是說線程Ⅱ在執行語句(4)時能夠觀測到線程Ⅰ在語句(5)時對LazySingleton的寫入值。接著對返回的 instance調用getSomeField()方法時,我們也能得到線程Ⅰ的語句(1) -> 線程Ⅱ的語句(7),這表明這時getSomeField能夠得到正確的值。但是僅僅是這種情況的正確性并不妨礙DCL的不正確性,一個程序的正確性必須 在所有的情況下的行為都是正確的,而不能有時正確,有時不正確。

?

對DCL的分析也告訴我們一條經驗原則, 對引用(包括對象引用和數組引用)的非同步訪問,即使得到該引用的最新值,卻并不能保證也能得到其成員變量(對數組而言就是每個數組元素)的最新值。

?

再稍微對DCL探討一下,這個例子中的LazySingleton是一個不變類,它只有get方法而沒有set方法。由對DCL的分析我們知道, 即使一個對象是不變的,在不同的線程中它的同一個方法也可能返回不同的值 。之所以會造成這個問題,是因為LazySingleton實例沒有被安全發布,所謂“被安全的發布”是指所有的線程應該在同步塊中獲得這個實例。這樣我們又得到一個經驗原則, 即使對于不可變對象,它也必須被安全的發布,才能被安全地共享。 所謂“安全的共享”就是說不需要同步也不會遇到數據競爭的問題。在Java5或以后,將someField聲明成final的,即使它不被安全的發布,也能被安全地共享,而在Java1.4或以前則必須被安全地發布。

?

關于DCL的修正

?

既然理解了DCL的根本原因,或許我們就可以修正它。

?

既然原因是線程Ⅱ執行getInstance()可能根本沒有在同步塊中執行,那就將整個方法都同步吧。這個毫無疑問是正確的,但是這卻回到最初的 起點(返樸歸真了),也完全違背了DCL的初衷,盡可能少的減少同步。雖然這不能帶任何意義,卻也說明一個道理,最簡單的往往是最好的。

?

如果我們嘗試不改動getInstance()方法,而是在getSomeField()上做文章,那么首先想到的應該是將getSomeField設置成同步,如下所示:

?

Java代碼 ? 收藏代碼
  1. public ? synchronized ? int ?getSomeField()?{??
  2. ???? return ? this .someField;???????????????????????????????? //?(7) ??
  3. }??

?

這種修改是不是正確的呢?答案是不正確。這是因為,第2條happen-before規則的前提條件并不成立。語句(5)所在同步塊和語句(7)所在同步塊并不是使用同一個鎖。像下面這樣修改才是對的:

Java代碼 ? 收藏代碼
  1. public ? int ?getSomeField()?{??
  2. ???? synchronized (LazySingleton. class )?{??
  3. ???????? return ? this .someField;??
  4. ????}??
  5. }??

但是這樣的修改雖然能保證正確性卻不能保證高性能。因為現在每次讀訪問getSomeField()都要同步,如果使用簡單的方法,將整個 getInstance()同步,只需要在getInstance()時同步一次,之后調用getSomeField()就不需要同步了。另外 getSomeField()方法也顯得很奇怪,明明是要返回實例變量卻要使用Class鎖。這也再次驗證了一個道理,簡單的才是好的。

?

好了,由于我的想象力有限,我能想到的修正也就僅限于此了,讓我們來看看網上提供的修正吧。

?

首先看Lucas Lee的修正( 這里 是原帖):

Java代碼 ? 收藏代碼
  1. private ? static ?LazySingleton?instance;??
  2. private ? static ? int ?hasInitialized?=? 0 ;??
  3. ??????
  4. public ? static ?LazySingleton?getInstance()?{??
  5. ???? if ?(hasInitialized?==? 0 )?{?????????????????????????????????????????? //?(4) ??
  6. ???????? synchronized (LazySingleton. class )?{????????????????????????? //?(5) ??
  7. ???????????? if ?(instance?==? null )?{????????????????????????????????? //?(6) ??
  8. ????????????????instance?=? new ?LazySingleton();????????????????????? //?(7) ??
  9. ????????????????hasInitialized?=? 1 ;??
  10. ????????????}??
  11. ????????}??
  12. ????}??
  13. ???? return ?instance;???????????????????????????????????????????????? //?(8) ??
  14. }??

如果你明白我前面所講的,那么很容易看出這里根本就是一個偽修正,線程Ⅱ仍然完全有可能在非同步狀態下返回instance。Lucas Lee的理由是對int變量的賦值是原子的,但實際上對instance的賦值也是原子的,Java語言規范規定對任何引用變量和基本變量的賦值都是原子 的,除了long和double以外。使用hasInitialized==0和instance==null來判斷LazySingleton有沒有初 始化沒有任何區別。Lucas Lee對 http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 中的最后一個例子有些誤解,里面的計算hashCode的例子之所以是正確的,是因為它返回的是int而不是對象的引用,因而不存在訪問到不正確成員變量值的問題。

?

neuzhujf的修正:

Java代碼 ? 收藏代碼
  1. public ? static ?LazySingleton?getInstance()?{??
  2. ???? if ?(instance?==? null )?{????????????????????????????????????????? //?(4) ??
  3. ???????? synchronized (LazySingleton. class )?{????????????????????????? //?(5) ??
  4. ???????????? if ?(instance?==? null )?{????????????????????????????????? //?(6) ??
  5. ????????????????LazySingleton?localRef?=? new ?LazySingleton();??
  6. ????????????????instance?=?localRef;???????????????????????? //?(7) ??
  7. ????????????}??
  8. ????????}??
  9. ????}??
  10. ???? return ?instance;???????????????????????????????????????????????? //?(8) ??
  11. }??

這里只是引入了一個局部變量,這也容易看出來只是一個偽修正,如果你弄明白了我前面所講的。

?

既然提到DCL,就不得不提到一個經典的而且正確的修正。就是使用一個static holder,kilik在回復中給出了這樣的一個修正。由于這里一種完全不同的思路,與我這里講的內容也沒有太大的關系,暫時略了吧。另外一個修正是使 用是threadlocal,都可以參見 這篇文章 。

?

步入Java5

?

前面所講的都是基于Java1.4及以前的版本,java5對內存模型作了重要的改 動,其中最主要的改動就是對volatile和final語義的改變。本文使用的happen-before規則實際上是從Java5中借鑒而來,然后再 移花接木到Java1.4中,因此也就不得不談下Java5中的多線程了。

?

在java 5中多增加了一條happen-before規則:

  • 對volatile字段的寫操作happen-before后續的對同一個字段的讀操作。

利用這條規則我們可以將instance聲明為volatile,即:

Java代碼 ? 收藏代碼
  1. private ? volatile ? static ?LazySingleton?instance;??

?根據這條規則,我們可以得到,線程Ⅰ的語句(5) -> 語線程Ⅱ的句(2),根據單線程規則,線程Ⅰ的語句(1) -> 線程Ⅰ的語句(5)和語線程Ⅱ的句(2) -> 語線程Ⅱ的句(7),再根據傳遞規則就有線程Ⅰ的語句(1) -> 語線程Ⅱ的句(7),這表示線程Ⅱ能夠觀察到線程Ⅰ在語句(1)時對someFiled的寫入值,程序能夠得到正確的行為。

?

在java5之前對final字段的同步語義和其它變量沒有什么區別,在java5中,final變量一旦在構造函數中設置完成(前提是在構造函數 中沒有泄露this引用),其它線程必定會看到在構造函數中設置的值。而DCL的問題正好在于看到對象的成員變量的默認值,因此我們可以將 LazySingleton的someField變量設置成final,這樣在java5中就能夠正確運行了。

?

?

遭遇同樣錯誤

?

在Java世界里,框架似乎做了很多事情來隱藏多線程,以至于很多程序員認為不再需要關注多線程了。 這實際上是個陷阱,這它只會使我們對多線程程序的bug反應遲鈍。大部分程序員(包括我)都不 會特別留意類文檔中的線程不安全警告,自己寫程序時也不會考慮將該類是否線程安全寫入文檔 中。做個測試,你知道java.text.SimpleDateFormat不是線程安全的嗎?如果你不知道,也不要感到奇怪,我也是在《Java Concurrent In Practice 》這書中才看到的。

?

現在我們已經明白了DCL中的問題,很多人都只認為這只不過是不切實際的理論者整天談論的話題,殊不知這樣的錯誤其實很常見。我就犯過,下面是從我同一個項目中所寫的代碼中摘錄出來的,讀者也不妨拿此來檢驗一下自己,你自己犯過嗎?即使沒有,你會毫不猶豫的這樣寫嗎?

?

第一個例子:

Java代碼 ? 收藏代碼
  1. public ? class ?TableConfig?{??
  2. ???? //.... ??
  3. ???? private ?FieldConfig[]?allFields;??
  4. ??????
  5. ???? private ? transient ?FieldConfig[]?_editFields;??
  6. ??
  7. ???? //.... ??
  8. ??????
  9. ???? public ?FieldConfig[]?getEditFields()?{??
  10. ???????? if ?(_editFields?==? null )?{??
  11. ????????????List<FieldConfig>?editFields?=? new ?ArrayList<FieldConfig>();??
  12. ???????????? for ?( int ?i?=? 0 ;?i?<?allFields.length;?i++)?{??
  13. ???????????????? if ?(allFields[i].editable)?editFields.add(allFields[i]);??
  14. ????????????}??
  15. ????????????_editFields?=?editFields.toArray( new ?FieldConfig[editFields.size()]);??
  16. ????????}??
  17. ???????? return ?_editFields;??
  18. ????}??
  19. }??

這里緩存了TableConfig的_editFields,免得以后再取要重新遍歷allFields。 這里存在和DCL同樣的問題,_editFields數組的引用可能是正確的值,但是數組成員卻可能null! 與DCL不同的是 ,由于對_editFields的賦值沒有同步,它可能被賦值多次,但是在這里沒有問題,因為每次賦值雖然其引用值不同,但是其數組成員是相同的,對于我 的業務來說,它們都等價的。由于我的代碼是要用在java1.4中,因此唯一的修復方法就是將整個方法聲明為同步。

?

第二個例子:

Java代碼 ? 收藏代碼
  1. private ?Map?selectSqls?=? new ?HashMap();??
  2. ??
  3. public ?Map?executeSelect( final ?TableConfig?tableConfig,?Map?keys)?{??
  4. ???? if ?(selectSqls.get(tableConfig.getId())?==? null )?{??
  5. ????????selectSqls.put(tableConfig.getId(),?constructSelectSql(tableConfig));??
  6. ????}??
  7. ????PreparedSql?psql?=?(PreparedSql)?selectSqls.get(tableConfig.getId());??
  8. ??
  9. ????List?result?=?executeSql(...);??
  10. ?????????
  11. ??????? return ?result.isEmpty()??? null ?:?(Map)?result.get( 0 );??
  12. ???}??
?

上面的代碼用constructSelectSql()方法來動態構造SQL語句,為了避免構造的開銷,將先前構造的結果緩存在 selectSqls這個Map中,下次直接從緩存取就可以了。顯然由于沒有同步,這段代碼會遭遇和DCL同樣的問題,雖然 selectSqls.get(...)可能能夠返回正確的引用,但是卻有可能返回該引用成員變量的非法值。另外selectSqls使用了非同步的 Map,并發調用時可能會破壞它的內部狀態,這會造成嚴重的后果,甚至程序崩潰??赡艿男迯途褪菍⒄麄€方法聲明為同步:

?

?

Java代碼 ? 收藏代碼
  1. public ? synchronized ?Map?executeSelect( final ?TableConfig?tableConfig,?Map?keys)??{??
  2. ???? //?.... ??
  3. ???}??

但是這樣馬上會遭遇吞吐量的問題,這里在同步塊執行了數據庫查詢,執行數據庫查詢是是個很慢的操作,這會導致其它線程執行同樣的操作時造成不必要的等待,因此較好的方法是減少同步塊的作用域,將數據庫查詢操作排除在同步塊之外:

Java代碼 ? 收藏代碼
  1. public ?Map?executeSelect( final ?TableConfig?tableConfig,?Map?keys)??{??
  2. ????PreparedSql?psql?=? null ;??
  3. ???? synchronized ( this )?{??
  4. ???? if ?(selectSqls.get(tableConfig.getId())?==? null )?{??
  5. ????????selectSqls.put(tableConfig.getId(),?constructSelectSql(tableConfig));??
  6. ????}??
  7. ????psql?=?(PreparedSql)?selectSqls.get(tableConfig.getId());??
  8. ????}??
  9. ??
  10. ????List?result?=?executeSql(...);??
  11. ??????
  12. ???? return ?result.isEmpty()??? null ?:?(Map)?result.get( 0 );??
  13. }??

現在情況已經改善了很多,畢竟我們將數據庫查詢操作拿到同步塊外面來了。但是仔細觀察會發現將this作為同步鎖并不是一個好主意,同步塊的目的是 保證從selectSqls這個Map中取到的是一致的對象,因此用selectSqls作為同步鎖會更好,這能夠提高性能。這個類中還存在很多類似的方 法executeUpdate,executeInsert時,它們都有自己的sql緩存,如果它們都采用this作為同步鎖,那么在執行 executeSelect方法時需要等待executeUpdate方法,而這種等待原本是不必要的。使用細粒度的鎖,可以消除這種等待,最后得到修改 后的代碼:

Java代碼 ? 收藏代碼
  1. private ?Map?selectSqls?=?Collections.synchronizedMap( new ?HashMap())??
  2. ??? public ?Map?executeSelect( final ?TableConfig?tableConfig,?Map?keys)??{??
  3. ????PreparedSql?psql?=? null ;??
  4. ???? synchronized (selectSqls)?{??
  5. ???????? if ?(selectSqls.get(tableConfig.getId())?==? null )?{??
  6. ????????????selectSqls.put(tableConfig.getId(),?constructSelectSql(tableConfig));??
  7. ????????}??
  8. ????????psql?=?(PreparedSql)?selectSqls.get(tableConfig.getId());??
  9. ????}??
  10. ??
  11. ????List?result?=?executeSql(...);??
  12. ?????????
  13. ??????? return ?result.isEmpty()??? null ?:?(Map)?result.get( 0 );??
  14. ???}??

我對selectSqls使用了同步Map,如果它只被這個方法使用,這就不是必須的。作為一種防范措施,雖然這會稍微降低性能,即便當它被其它方 法使用了也能夠保護它的內部結構不被破壞。并且由于Map的內部鎖是非競爭性鎖,根據官方說法,這對性能影響很小,可以忽略不計。這里我有意無意地提到了 編寫高性能的兩個原則, 盡量減少同步塊的作用域,以及使用細粒度的鎖 ,關于細粒度鎖的最經典例子莫過于讀寫鎖了。這兩個原則要慎用,除非你能保證你的程序是正確的。

?

?

結束語

?

在這篇文章中我主要講到happen-before規則,并運用它來分析DCL問題,最后我用例子來說明DCL問題并不只是理論上的討論,在實際程 序中其實很常見。我希望讀者能夠明白用happen-before規則比使用時間的先后順序來分析線程安全性要有效得多,作為對比,你可以看看這篇 經典的文章 中是如何分析DCL的線程安全性的。它是否講明白了呢?如果它講明白了,你是否又能理解?我想答案很可能是否定的,不然的話就不會出現這么多對DCL的誤 解了。當然我也并不是說要用happen-before規則來分析所有程序的線程安全性,如果你試著分析幾個程序就會發現這是件很困難的事,因為這個規則 實在是太底層了,要想更高效的分析程序的線程安全性,還得總結和利用了一些高層的經驗規則。關于這些經驗規則,我在文中也談到了一些,很零碎也不完全。

用happen-before規則重新審視DCL


更多文章、技術交流、商務合作、聯系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯系: 360901061

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

【本文對您有幫助就好】

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

發表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 妞干网免费在线观看 | 欧美在线观看a | 欧美黑人疯狂性受xxxxx喷水 | 加勒比久草 | av天空 | 白天操夜夜操 | 看特级毛片 | 丰满少妇久久久久久久 | 超级碰97| 国产乱子轮XXX农村 日本中文字幕一区 | 久章草影院 | 欧美天堂 | 天天综合亚洲 | 奇米视频888| 欧美精品久久久久久久久久 | 欧美精品3atv一区二区三区 | 日本在线不卡视频 | 精品欧美高清一区二区免费 | 日本精品久久久久中文字幕2 | 欧美精品免费线视频观看视频 | 深夜爽爽爽gif福利免费 | 天天摸天天做天天爽 | 欧美精品久久久久久久久久 | 欧美日韩中文字幕在线视频 | 污污的网站免费在线观看 | 亚洲成人在线免费 | 亚洲一区二区三区视频 | 天干夜天天夜天干天国产电影 | 色撸网 | 国产精品一区久久久 | 欧美电影一区 | 国产精品成人第一区 | 免费区一级欧美毛片 | 国产网站在线 | 欧美久 | 人人艹逼| 91麻豆精品一区二区三区 | 国产人成精品 | 亚洲精品久久午夜无码一区二区 | 最全精品自拍视频在线 | 人人爱国产 |