天天看點

volatile足以保證資料同步嗎

在讨論之前必須先搞清四種存儲媒體:寄存器、進階緩存、RAM和ROM。

RAM與ROM大家都比較熟悉了,可以看成是我們經常說的記憶體與硬碟,寄存器屬于處理器裡面的一部分,而進階緩存cache是CPU設計者為提高性能引入的一個緩存,也可以說是屬于處理器的一部分。在利用CPU進行運算時必定涉及操作數的讀取,假如CPU直接讀取ROM,那麼這個讀取速度簡直是無法忍受的,于是引入了記憶體RAM,這樣做确實讓速度提高了很多,但由于CPU發展十分迅猛而另一方面RAM的發展受到技術及成本的限制發展緩慢,此時産生了一個很難調和的沖突,CPU運算速度比從RAM讀取資料的速度快了幾個數量級,木桶原理我們都很熟悉了,桶的容量大小取決于最短的那塊,這必将影響處理器的效率,于是又引入了進階緩存,直接在CPU添加了幾個級别的緩存,他們的速度雖然無法與寄存器比較,但是速度已經提升很多,基本能跟CPU的計算速度相比對。總結成一句話就是,為了解決CPU運算速度與讀取速度的沖突,引入了多級存儲機制。

如圖所示,機器的四種存儲媒體是有關系的,一般程式運作時會将ROM相關的程式資料都讀進RAM中,而需要運算的資料或運算過程中即将要用到的資料則會被讀進高速緩存或寄存器中,假如要進行的運算所需要的所有資料及指令都在寄存器和高速緩存中,則這個運算過程則表現得非常平坦,并不存在性能瓶頸,因為運算速度跟讀取速度基本比對了。讀取速度快慢的排序如下:寄存器>cache>RAM>ROM,用一個比較好了解但不完全正确的概念來解釋,因為寄存器是離CPU最近的,是以讀取最快,高速緩存次之,RAM第三,ROM離得最遠,自然速度最慢(當然不能完全用距離來說明這個問題,但用距離是比較好了解的,另外的還因為這些存儲媒體的硬體設計不同、工作方式不同)。從另一個角度來看,CPU讀取資料的順序是先嘗試讀寄存器,如果不存在則嘗試讀高速緩存,如果還不存在則讀RAM,最後才是讀ROM。一些CPU有三級cache,讀取時是一級一級往下直到找到需要的操作數,一般做的比較好的CPU3級緩存已經能讓命中率高達95%以上。

有了上面的知識再往下探索就水到渠成了,如果把Java記憶體模型與多級存儲機制類比将發現為了提高性能java引入了工作記憶體的概念,提高了線程執行時讀取資料的速度,這樣就可以把java模型中的主存和工作記憶體分别于RAM和高速緩存或寄存器對應起來,每條線程的工作記憶體預先把需要的資料複制到高速緩存或寄存器(但是不保證所有的工作記憶體的變量副本都是放在高速緩存,也可能在RAM,具體的還要看JVM是如何實作的),這樣在多線程并發時性能得到保證。當然寄存器和高速緩存由于成本原因存在容量大小限制的問題,這個也是考驗JVM實作的一個難題。

一般引入一種機制解決了一個問題,但同時也會帶來另外一個問題,資料同步即是帶來的另一個問題,即是否能保證目前運算使用的變量值總是目前時刻最新的值。如果變量值并非最新值,将會導緻資料的髒讀,最終可能導緻計算結果大相徑庭。這時可能有人會想起java中有個volatile關鍵詞,毫無疑問它能保證可見性,讓每個線程得到的都是主存中最新的變量值,但它就足以保證資料的同步性了嗎?舉個典型例子,僞代碼如下:

執行完所有線程任務,我們期望的結果會是30*10000,但實際卻是一個小于30*10000的數,剛開始看到一定覺得有點奇怪,但仔細一想就清楚了,count++編譯後最終并非一個原子操作,它由幾個指令一起組合實作。下圖能較清晰地說明此點,在Java記憶體模型中,count++被分割成5個步驟(當然這個并不是确切的指令執行步驟),這5步不具有原子性,假如在完成過程中,其他線程就去讀了主存的count變量,那明顯導緻了一個髒讀現象。

導緻這個問題的原因其實是因為volatile不具備鎖操作,要解決此問題其實不難,就是将這五步變為原子操作,即保證線程一完成之前不能有其他線程讀取count變量,對count變量加一個互斥鎖即可達到,線程一在執行第①步前對count加鎖,其他線程無法對count進行通路,線程一執行完第⑤步後釋放鎖,此刻開始才允許其他線程擷取此變量。

Volatile是一個很容易搞混的關鍵詞,很多經驗豐富的開發人員都不能正确使用它,這節從機器結構講到對應的java記憶體模型,再引出主存與工作記憶體之間資料同步的問題,進而更好地解釋了volatile的确切含義——它隻保證可見性,它不足以保證資料的同步性。

========廣告時間========

<a href="http://blog.csdn.net/wangyangzhizhou/article/details/74080321">為什麼寫《Tomcat核心設計剖析》</a>

=========================

歡迎關注: