深入分析Volatile的實現原理

zhoubangding發表於2017-02-16

本文屬於作者原創,原文發表於InfoQ:http://www.infoq.com/cn/articles/ftf-java-volatile

引言

在多執行緒併發程式設計中synchronized和Volatile都扮演著重要的角色,Volatile是輕量級的synchronized,它在多處理器開發中保證了共享變數的“可見性”。可見性的意思是當一個執行緒修改一個共享變數時,另外一個執行緒能讀到這個修改的值。它在某些情況下比synchronized的開銷更小,本文將深入分析在硬體層面上Inter處理器是如何實現Volatile的,通過深入分析能幫助我們正確的使用Volatile變數。

術語定義

術語 英文單詞 描述
共享變數   在多個執行緒之間能夠被共享的變數被稱為共享變數。共享變數包括所有的例項變數,靜態變數和陣列元素。他們都被存放在堆記憶體中,Volatile只作用於共享變數。
記憶體屏障 Memory Barriers 是一組處理器指令,用於實現對記憶體操作的順序限制。
快取行 Cache line 快取中可以分配的最小儲存單位。處理器填寫快取線時會載入整個快取線,需要使用多個主記憶體讀週期。
原子操作 Atomic operations 不可中斷的一個或一系列操作。
快取行填充 cache line fill 當處理器識別到從記憶體中讀取運算元是可快取的,處理器讀取整個快取行到適當的快取(L1,L2,L3的或所有)
快取命中 cache hit 如果進行快取記憶體行填充操作的記憶體位置仍然是下次處理器訪問的地址時,處理器從快取中讀取運算元,而不是從記憶體。
寫命中 write hit 當處理器將運算元寫回到一個記憶體快取的區域時,它首先會檢查這個快取的記憶體地址是否在快取行中,如果存在一個有效的快取行,則處理器將這個運算元寫回到快取,而不是寫回到記憶體,這個操作被稱為寫命中。
寫缺失 write misses the cache 一個有效的快取行被寫入到不存在的記憶體區域。

Volatile的官方定義

Java語言規範第三版中對volatile的定義如下: java程式語言允許執行緒訪問共享變數,為了確保共享變數能被準確和一致的更新,執行緒應該確保通過排他鎖單獨獲得這個變數。Java語言提供了volatile,在某些情況下比鎖更加方便。如果一個欄位被宣告成volatile,java執行緒記憶體模型確保所有執行緒看到這個變數的值是一致的。

為什麼要使用Volatile

Volatile變數修飾符如果使用恰當的話,它比synchronized的使用和執行成本會更低,因為它不會引起執行緒上下文的切換和排程。

Volatile的實現原理

那麼Volatile是如何來保證可見性的呢?在x86處理器下通過工具獲取JIT編譯器生成的彙編指令來看看對Volatile進行寫操作CPU會做什麼事情。

Java程式碼: instance = new Singleton();//instance是volatile變數
彙編程式碼: 0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);

有volatile變數修飾的共享變數進行寫操作的時候會多第二行彙編程式碼,通過查IA-32架構軟體開發者手冊可知,lock字首的指令在多核處理器下會引發了兩件事情。

  • 將當前處理器快取行的資料會寫回到系統記憶體。
  • 這個寫回記憶體的操作會引起在其他CPU裡快取了該記憶體地址的資料無效。
處理器為了提高處理速度,不直接和記憶體進行通訊,而是先將系統記憶體的資料讀到內部快取(L1,L2或其他)後再進行操作,但操作完之後不知道何時會寫到記憶體,如果對宣告瞭Volatile變數進行寫操作,JVM就會向處理器傳送一條Lock字首的指令,將這個變數所在快取行的資料寫回到系統記憶體。但是就算寫回到記憶體,如果其他處理器快取的值還是舊的,再執行計算操作就會有問題,所以在多處理器下,為了保證各個處理器的快取是一致的,就會實現快取一致性協議每個處理器通過嗅探在匯流排上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器要對這個資料進行修改操作的時候,會強制重新從系統記憶體裡把資料讀到處理器快取裡

這兩件事情在IA-32軟體開發者架構手冊的第三冊的多處理器管理章節(第八章)中有詳細闡述。

Lock字首指令會引起處理器快取回寫到記憶體。Lock字首指令導致在執行指令期間,聲言處理器的 LOCK# 訊號。在多處理器環境中,LOCK# 訊號確保在聲言該訊號期間,處理器可以獨佔使用任何共享記憶體。(因為它會鎖住匯流排,導致其他CPU不能訪問匯流排,不能訪問匯流排就意味著不能訪問系統記憶體),但是在最近的處理器裡,LOCK#訊號一般不鎖匯流排,而是鎖快取,畢竟鎖匯流排開銷比較大。在8.1.4章節有詳細說明鎖定操作對處理器快取的影響,對於Intel486和Pentium處理器,在鎖操作時,總是在匯流排上聲言LOCK#訊號。但在P6和最近的處理器中,如果訪問的記憶體區域已經快取在處理器內部,則不會聲言LOCK#訊號。相反地,它會鎖定這塊記憶體區域的快取並回寫到記憶體,並使用快取一致性機制來確保修改的原子性,此操作被稱為“快取鎖定”,快取一致性機制會阻止同時修改被兩個以上處理器快取的記憶體區域資料

一個處理器的快取回寫到記憶體會導致其他處理器的快取無效。IA-32處理器和Intel 64處理器使用MESI(修改,獨佔,共享,無效)控制協議去維護內部快取和其他處理器快取的一致性。在多核處理器系統中進行操作的時候,IA-32 和Intel 64處理器能嗅探其他處理器訪問系統記憶體和它們的內部快取。它們使用嗅探技術保證它的內部快取,系統記憶體和其他處理器的快取的資料在匯流排上保持一致。例如在Pentium和P6 family處理器中,如果通過嗅探一個處理器來檢測其他處理器打算寫記憶體地址,而這個地址當前處理共享狀態,那麼正在嗅探的處理器將無效它的快取行,在下次訪問相同記憶體地址時,強制執行快取行填充。

-----------------------------------------------------------------  分割線  ----------------------------------------------------------------------

總結:

volatile是輕量級的synchronized,他們都在併發程式設計中扮演著重要的角色。volatile保證了共享變數的“可見性”。通常,volatile的使用成本比synchronized更小,因為使用volatile不會引起執行緒的上下文切換。

共享變數的可見性如何保證?

使用volatile修飾的屬性,在生成的彙編指令中,會有一個lock字首,lock字首在多核處理器下會保證兩件事:1.將當前CPU的快取行資料回寫到系統記憶體中。在回寫記憶體時,會使用快取鎖定操作;2.這個回寫記憶體操作會引起其它CPU快取中該資料無效,主要通過快取一致性機制保證。

相關文章