Java併發系列之volatile

架構師springboot發表於2018-11-07

講到Java併發,多執行緒程式設計,一定避免不了對關鍵字volatile的瞭解,那麼如何來認識volatile,從哪些方面來了解它會比較合適呢?

個人認為,既然是多執行緒程式設計,那我們在平常的學習中,工作中,大部分都接觸到的就是執行緒安全的概念。

而執行緒安全就會涉及到共享變數的概念,所以首先,我們得弄清楚共享變數是什麼,且處理器和記憶體間的資料互動機制是如何導致共享變數變得不安全。

共享變數 能夠在多個執行緒間被多個執行緒都訪問到的變數,我們稱之為共享變數。共享變數包括所有的例項變數,靜態變數和陣列元素。他們都被存放在堆記憶體中。

處理器與記憶體的通訊機制 大家都知道處理器是用來做計算的,且速度是非常快的,而記憶體是用來儲存資料的,且其訪問速度相比處理器來說,是慢了好幾個級別的。那麼當處理器需要處理資料時,如果每次都直接從記憶體拿資料的話,就會導致效率非常低,因此在現代計算機系統中,處理器是不直接跟記憶體通訊的,而是在處理器和記憶體之間設定了多個快取,也就是我們常常聽到的L1, L2, L3等快取記憶體。

具體架構如下所示:

Java併發系列之volatile
處理器都是將資料從記憶體讀到自己內部的快取中,然後在快取中對資料進行修改等操作,結束後再由快取寫到回主存中去。 如果一個共享變數 X,在多執行緒的情況下,同時被多個處理器讀到各自的快取中去,當其中一個處理器修改了X的值,改成Y了,先寫回了記憶體,而此時另外一個處理器,又將X改成Z,再寫回記憶體,那麼之前的Y就會被覆蓋掉了。

這種情況下,資料就已經有問題了,這種因為多執行緒操作而導致的異常問題,通常我們就叫做執行緒不安全。

Java併發系列之volatile

Java併發系列之volatile
如上述兩圖所示,X的變數同時被不同的處理器修改成各自的Y和Z,那麼如何避免這種情況呢? 這就涉及到了Java記憶體模型中的可見性的概念。

Java記憶體模型之可見性 可見性,意思就是說,在多執行緒程式設計中,某個共享變數在其中一個執行緒被修改了,其修改結果要馬上能夠被其他執行緒看到,拿上面的例子來說,也就是當X在其中一個處理器的快取中被修改成Y了, 另一個處理器必須能夠馬上知道自己快取中的X已經被修改成Y了,當此處理器要拿此變數去參與計算的時候,必須重新去記憶體中將此變數的值Y讀到快取中。

而一個變數,如果被宣告成violate,那麼其就能保證這種可見性,這就是volatile變數的作用了。

volatile 那麼 volatile 變數能夠保證可見性的實現原理是什麼? 宣告成volatile的變數,在編譯成彙編指令的時候,會多出以下一行:

0x0bca13ae:lock addl $0x0,(%esp) ; 這一句指令的意思是在暫存器上做一個+0的空操作,但這條指令有個Lock字首。 而處理器在處理Lock字首指令時,其實是聲言了處理器的Lock#訊號。 在之前的處理器中,Lock#訊號會導致傳輸資料的匯流排被鎖定,其他處理器都不能訪問匯流排,從而保證處理Lock指令的處理器能夠獨享運算元據所在的記憶體區域。

但由於匯流排被鎖住,其他的處理器都被堵住了,影響多處理器執行的效率。在後來的處理器中,聲言Lock#訊號的處理器,不會再鎖住匯流排,而是檢查到資料所在的記憶體區域,如果是在處理器的內部快取中,則會鎖定此快取區域,將快取寫回到記憶體當中,並利用快取一致性的原則來保證其他處理器中的快取區域資料的一致性。

快取一致性 快取一致性原則會保證一個在快取中的資料被修改了,會保證其他快取了此資料的處理器中的快取失效,從而讓處理器重新去記憶體中讀取最新修改後的資料。

在實際的處理器操作中,各個處理器會一直在匯流排上嗅探其內部快取區域中的記憶體地址在其它處理器的操作情況,一旦嗅探到某處理器打算修改某記憶體地址,而此記憶體地址剛好也在自己內部的快取中,則會強制讓自己的快取無效。當下次訪問此記憶體地址的時候,則重新從記憶體當中讀取新資料。

volatile不僅保證了共享變數在多執行緒間的可見性,其還保證了一定的有序性。

有序性 何謂有序性呢? 事實上,java程式程式碼在編譯器階段和處理器執行階段,為了優化執行的效率,有可能會對指令進行重排序。 如果一些指令彼此之間互相不影響,那麼就有可能不按照程式碼順序執行,比如後面的程式碼先執行,而之前的程式碼則慢執行,但處理器會保證結束時的輸出結果是一致的。 以上的這種情況就說明指令有可能不是有序的。

volatile變數,上面我們看過其彙編指令,會多出一條Lock字首的指令,這條指令能夠 保證,在這條指令之前的所有指令全部執行完畢,而在這條指令之後的所有指令全部未執行,也相於在這裡立起了一道柵欄,稱之為記憶體柵欄,而更通俗的說法,則是記憶體屏障。

那麼有了這道屏障,volatile變數就禁止了指令的重排序,從而保證了指令執行的有序性。

所有對volatile變數的讀操作一定發生在對volatile變數的寫操作之後。這同時也說明了volatile變數在多個執行緒之間能夠實現可見性的原理。所以各種規定和操作,其實之間互有關聯,彼此依賴,才能更好地保證指令執行的準確和效率。

記憶體屏障 在上面我們也引出了記憶體屏障的概念,也知道了,其實它就是一組處理器的操作指令。

插入一個記憶體屏障,則相當於告訴處理器和編譯器先於這個指令的必須先執行,後於這個指令的必須後執行。

Java併發系列之volatile
記憶體屏障另一個作用是強制更新一次不同CPU的快取。

例如,一個寫屏障會把這個屏障前寫入的資料重新整理到快取,這樣任何試圖讀取該資料的執行緒將得到最新值,而不用考慮到底是被哪個cpu核心或者哪顆CPU執行的。

這再仔細一想,不就是上面所說的volatile的作用嗎?

所以,記憶體屏障,可見性,有序性,快取一致性原則,在java併發中各種各樣的名詞,本質上可能就只是同一種現象或者同一種設計,從不同的角度觀察和探討所得出的不同的解釋。 加Java架構師群獲取Java工程化、高效能及分散式、高效能、深入淺出。高架構。效能調優、Spring,MyBatis,Netty原始碼分析和大資料等多個知識點高階進階乾貨的直播免費學習許可權 都是大牛帶飛 讓你少走很多的彎路的 群..號是:855801563 對了 小白勿進 最好是有開發經驗

注:加群要求

1、具有工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加。

2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加。

3、如果沒有工作經驗,但基礎非常紮實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的,可以加。

4、覺得自己很牛B,一般需求都能搞定。但是所學的知識點沒有系統化,很難在技術領域繼續突破的可以加。

5.阿里Java高階大牛直播講解知識點,分享知識,多年工作經驗的梳理和總結,帶著大家全面、科學地建立自己的技術體系和技術認知!

相關文章