轉載自 ---- http://www.iteye.com/topic/875420
????? 如果你搜索網上分析dcl為什么在java中失效的原因,都會談到編譯器會做優化云云,我相信大家看到這個一定會覺得很沮喪、很無助,對自己寫的 程序很沒信心。我很理解這種感受,因為我也經歷過,這或許是為什么網上一直有人喜歡談dcl的原因。如果放在java5之前,從編譯器的角度去解釋dcl 也無可厚非,在java5的JMM(內存模型)已經得到很大的修正,如果到現在還只能從編譯器的角度去解釋dcl,那簡直就在污辱java,要知道 java的最大優勢就是只需要考慮一個平臺。你可以完全無視網上絕大多數關于dcl的討論,很多時候他們自己都說不清楚,除Doug Lea等幾個大牛,我不相信誰比誰更權威。
很多人不理解dcl,不是dcl有多么復雜,恰恰相反,而是對基礎掌握得不夠。所以,我會先從基礎講起,然后再分析DCL。
我們都知道,當兩個線程同時讀寫(或同時寫)一個共享變量時會發生數據競爭。那我怎么才能知道發生了數據競爭呢?我需要去讀取那個變量,發生數據 競爭通常有兩個表現:一是讀取到陳舊數據,即讀取到雖是曾經寫入的數據,但不是最新的。二是讀取到之前根本沒有寫入的值,也就是說讀到垃圾。
數據陳舊性
為了讀取到另一個線程寫入的最新數據,JMM定義了一系列的規則,最基本的規則就是要利用同步。在Java中,同步的手段有synchronized和volatile兩種,這里我只會涉及到syncrhonized。請大家先記住以下規則,接下來我會細講。
規則一:必須對變量的所有寫和所有讀同步,才能讀取到該最新的數據。
先看下面的代碼:
- public ? class ?A?{??
- ???? private ? int ?some;??
- ???? public ? int ?another;??
- ??
- ???? public ? int ?getSome()?{? return ?some;?}??
- ???? public ? synchronized ? int ?getSomeWithSync()?{? return ?some;?}??
- ???? public ? void ?setSome( int ?v)?{?some?=?v;?}??
- ???? public ? synchronized ? void ?setSomeWithSync( int ?v)?{?some?=?v;?}??
- }??
讓我們來分析一個線程寫,另一個線程讀的情形,一共四種情形。初始情況都是a = new A(),暫不考慮其它線程。
情形一:讀寫都不同步。
Thread1 | Thread2 |
(1) a.setSome(13) | |
(2) a.getSome() |
這種情況下,即使thread1先寫入some為13,thread2再讀取some,它能讀到13嗎?在沒有同步協調下,結果是不確定的。從圖 上看出,兩個線程獨立運行,JMM并不保證一個線程能夠看到另一個線程寫入的值。在這個例子中,就是thread2可能讀到0(即some的初始值)而不 是13。注意,在理論上,即使thread2在thread1寫入some之后再等上一萬年也還是可能讀到some的初始值0,盡管這在實際幾乎不可能發 生。
情形二:寫同步,讀不同步
Thread1 | Thread2 |
(1) a.setSomeWithSync(13) | |
(2) a.getSome() |
Thread1 | Thread2 |
(1) a.setSome(13) | |
(2) a.getSomeWithSync() |
在這兩種情況下,thread1和thread2只對讀或只對寫some加了鎖,這不起任何作用,和[情形一]一樣,thread2仍有可能讀到 some的初始值0。從圖上也可看出,thread1和thread2互相之間并沒有任何影響,一方加鎖并不影響另一方的繼續運行。圖中也顯示,同步操作 相當于在同步開始執行lock操作,在同步結束時執行unlock操作。
情形四:讀寫都同步
Thread1 | Thread2 |
(1) a.setSomeWithSync(13) | |
(2) a.getSomeWithSync() |
在情形四中,thread1寫入some時,thread2等待thread1寫入完成,并且它能看到thread1對some做的修改,這時 thread2保證能讀到13。實際上,thread2不僅能看到thread1對some的修改,而且還能看到thread1在修改some之前所做的 任何修改。說得更精確一些,就是一個線程的lock操作能看見另一線程對同一個對象unlock操作之前的所有修改,請注意圖中的紅色箭頭。 沿著圖中箭頭指示方向,箭頭結尾處總能看到箭頭開始處操作做的修改。這樣,a.some[thread2]能看見 lock[thread2],lock[thread2]能看見unlock[thread1],unlock[thread1]又能看見 a.some=13[thread1],即能看到some的值為13。
再來看一個稍微復雜一點的例子:
例子五
Thread1 | Thread2 |
(1) a.another = 5 | |
(2) a.setSomeWithSync(13) | |
(3) a.getSomeWithSync() | |
(4) a.another = 7 | |
(5) a.another |
thread2最后會讀到another的什么值呢?會不會讀到another的初始值0呢,畢竟所有對another的訪問都沒有同步?不會。 從圖中很清晰地可以看出,thread2的another至少到看到thread1在lock之前寫入的5,卻并不能保證它能看到thread1在 unlock寫入的7。因此,thread2可以什么讀到another的值可能5或7,但不會是0。你或許已經發現,如果去掉圖中thread2讀取 a.some的操作,這時相當于一個空的同步塊,對結論并沒有任何影響。這說明空的同步塊是起作用的,編譯器不能擅自將空的同步塊優化掉,但你在使用空的 同步塊應該特別小心,通常它都不是你想要的結果。另外需要注意,unlock操作和lock操作必須針對同一個對象,才能保證unlock操作能看到 lock操作之前所做的修改。
例子六:不同的鎖
- class ?B?{??
- ???? private ?Object?lock1?=? new ?Object();??
- ???? private ?Object?lock2?=? new ?Object();??
- ??
- ???? private ? int ?some;??
- ??
- ???? public ? int ?getSome()?{??
- ???????? synchronized (lock1)?{? return ?some;?}??
- ????}??
- ??
- ???? public ? void ?setSome( int ?v)?{??
- ???????? synchronized (lock2)?{?some?=?v;?}??
- ????}??
- }??
?
Thread1 | Thread2 |
(1) b.setSome(13) | |
(2) b.getSome() |
在這種情況下,雖然getSome和setSome都加了鎖,但由于它們是不同的鎖,一個線程運行時并不能阻塞另一個線程運行。因此這里的情形和情形一、二、三一樣,thread2不保證讀到thread1寫入的some最新值。
現在來看DCL:
例子七: DCL
- public ? class ?LazySingleton?{??
- ???? private ? int ?someField;??
- ??????
- ???? private ? static ?LazySingleton?instance;??
- ??????
- ???? private ?LazySingleton()?{??
- ???????? this .someField?=? 201 ;????????????????????????????????? //?(1) ??
- ????}??
- ??????
- ???? public ? static ?LazySingleton?getInstance()?{??
- ???????? if ?(instance?==? null )?{??????????????????????????????? //?(2) ??
- ???????????? synchronized (LazySingleton. class )?{??????????????? //?(3) ??
- ???????????????? if ?(instance?==? null )?{??????????????????????? //?(4) ??
- ????????????????????instance?=? new ?LazySingleton();??????????? //?(5) ??
- ????????????????}??
- ????????????}??
- ????????}??
- ???????? return ?instance;?????????????????????????????????????? //?(6) ??
- ????}??
- ??????
- ???? public ? int ?getSomeField()?{??
- ???????? return ? this .someField;???????????????????????????????? //?(7) ??
- ????}??
- }??
假設thread1先調用getInstance(),由于此時還沒有任何線程創建LazySingleton實例,它會創建一個實例s并返回。 這是thread2再調用getInstance(),當它運行到(2)處,由于這時讀instance沒有同步,它有可能讀到s或者null(參考情形 二)。先考慮它讀到s的情形,畫出流程圖就是下面這樣的:
由于thread2已經讀到s,所以getInstance()會立即返回s,這是沒有任何問題,但當它讀取s.someFiled時問題就發生 了。 從圖中可以看thread2沒有任何同步,所以它可能看不到thread1寫入someField的值20,對thread2來說,它可能讀到 s.someField為0,這就是DCL的根本問題。從上面的分析也可以看出,為什么試圖修正DCL但又希望完全避免同步的方法幾乎總是行不通的。
接下來考慮thread2在(2)處讀到instance為null的情形,畫出流程圖:
接下來thread2會在有鎖的情況下讀取instance的值,這時它保證能讀到s,理由參考情形四或者通過圖中箭頭指示方向來判定。
關于DCL就說這么多,留下兩個問題:
- 接著考慮thread2在(2)讀到instance為null的情形,它接著調用s.someFiled會得到什么?會得到0嗎?
- DCL為什么要double check,能不能去掉(4)處的check?若不能,為什么?
原子性
回到情形一,為什么我們說thread2讀到some的值只可能為為0或13,而不可能為其它?這是由java對int、引用讀寫都是原子性所決 定的。所謂“原子性”,就是不可分割的最小單元,有數據庫事務概念的同學們應該對此容易理解。當調用some=13時,要么就寫入成功要么就寫入失敗,不 可能寫入一半。但是,java對double, long的讀寫卻不是原子操作,這意味著可能發生某些極端意外的情況??蠢樱?
- public ? class ?C?{??
- ???? private ? /*?volatile?*/ ? long ?x;??????????????????????????? //?(1) ??
- ??
- ???? public ? void ?setX( long ?v)?{?x?=?v;?}??
- ???? public ? long ?getX()?{? return ?x;?}??
- }??
?
Thread1 | Thread2 |
(1) c.setX(0x1122334400112233L) | |
(2) c.getX() |
thread2讀取x的值可能為0,1122334400112233外,還可能為別的完全意想不到的值。一種情況假設jvm對long的寫入是 先寫低4字節,再寫高4字節,那么讀取到x的值還可能為112233。但是我們不對jvm做如此假設,為了保證對long或double的讀寫是原子操 作,有兩種方式,一是使用volatile,二是使用synchronized。對上面的例子,如果取消(1)處的volatile注釋,將能保證 thread2讀取到x的值要么為0,要么為1122334400112233。如果使用同步,則必須像下面這樣對getX,setX都同步:
- public ? class ?C?{??
- ???? private ? /*?volatile?*/ ? long ?x;??????????????????????????? //?(1) ??
- ??
- ???? public ? synchronized ? void ?setX( long ?v)?{?x?=?v;?}??
- ???? public ? synchronized ? long ?getX()?{? return ?x;?}??
- }??
因此對原子性也有規則(volatile其實也是一種同步)。
規則二:對double, long變量,只有對所有讀寫都同步,才能保證它的原子性
有時候我們需要保證一個復合操作的原子性,這時就只能使用synchronized。
- public ? class ?Canvas?{??
- ???? private ? int ?curX,?curY;??
- ??
- ???? public ? /*?synchronized?*/ ?getPos()?{??
- ???????? return ? new ? int []?{?curX,?curY?};??
- ??????????
- ????}??
- ??
- ???? public ? /*?synchronized?*/ ? void ?moveTo( int ?x,? int ?y)?{??
- ????????curX?=?x;??
- ????????curY?=?y;??
- ????}??
- }??
?
Thread1 | Thread2 |
(1) c.moveTo(1, 1) | |
(2) c.moveTo(2, 2) | |
(3) c.getPos() |
當沒有同步的情況下,thread2的getPos可能會得到[1, 2], 盡管該點可能從來沒有出現過。之所以會出現這樣的結果,是因為thread2在調用getPos()時,curX有0,1或2三種可能,同樣curY也有 0,1或2三種可能,所以getPos()可能返回[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]共九種可能。要避免這種情況,只有將getPos()和moveTo都設為同步方法。
總結
以上分析了數據競爭的兩種癥狀,陳舊數據和非原子操作,都是由于沒有恰當同步引起的。這些其實都是相當基礎的知識,同步可有兩種效果:一是保證讀 取最新數據,二是保證操作原子性,但是大多數書籍都對后者過份強調,對前者認識不足,以致對多線程的認識上存在很多誤區。如果想要掌握java線程高級知 識,我只推薦《Java并發編程設計原則與模式》。其實我已經好久沒有寫Java了,這些東西都是我兩年前的知識,如果存在問題,歡迎大家指出,千萬不要 客氣。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

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