併發程式設計(二) volatile的應用
在閱讀別人程式碼的時候,能夠看到volatile關鍵字,由於自己用的少,查詢過一些網上的資料,理解也並不透徹。今天結合書上的知識重新梳理一遍volatile的實現原理。
* volatile是輕量級的synchronized,它保證了共享變數在多處理器開發中的“可見性”。
* 什麼是可見性呢?可見性的意思是當一個執行緒修改一個共享變數時,另一個執行緒能讀取到這個修改的值。
* 如果volatile使用得當,它會比synchronized的使用和執行成本更低,因為不會引起上下文的切換。
* volatile的定義:Java程式語言允許執行緒訪問共享變數,為了確保共享變數能被準確和一致地更新,執行緒應該確保通過排他鎖單獨或者這個變數。
* 如果一個欄位被申明誠volatile,Java執行緒記憶體模型確保所有執行緒看到這個變數的值是一致的。
以上給出了volatile的相關定義和內涵,繼續往下看,下表還是給出了與實現volatile相關的CPU術語:
術語 | 英文單詞 | 術語描述 |
---|---|---|
記憶體屏障 | memory barriers | 是一組處理器指令,用於實現對記憶體操作的順序限制 |
緩衝行 | cache line | CPU快取記憶體中可以分配的最小儲存單位,處理器填寫快取行時會載入整個快取行,現代CPU需要執行幾百次CPU指令 |
原子操作 | atomic operations | 不可中斷的一個或一系列操作 |
快取行填充 | cache line fill | 當處理器識別到從記憶體中讀取運算元是可快取的,處理器讀取整個快取記憶體行到適當的快取(L1,L2,L3的或所有) |
快取命中 | cache hit | 如果進行快取記憶體行填充操作的記憶體位置仍然是下次處理器訪問的地址時,處理器從快取中讀取運算元,而不是從記憶體讀取 |
寫命中 | write hit | 當處理器將運算元寫回到一個記憶體快取的區域時,它會首先檢查這個快取地址是否在快取行中,如果存在一個有效的快取行,則處理器將這個運算元寫回到快取,這個操作被稱為寫命中 |
寫缺失 | write miss the cache | 一個有效的快取行被寫入到不存在的記憶體區域 |
看完上面的定義以後,對於對CPU術語不太熟悉的朋友來說,肯定是一頭霧水。
那麼我們看看在X86架構下的CPU在面對volatile關鍵字時會做什麼事情:
Java程式碼:
instance = new Singleton(); // instance 是volatile修飾
經過jvm轉變成彙編程式碼後:
0x01a3deld: move
有volatile修飾的共享變數進行寫操作的時候會多出第二行彙編程式碼,Lock字首的指令在多核處理器下回引發兩件事情:
1. 將當前處理器快取行的資料寫回系統記憶體。
2. 這個寫回記憶體的操作會使其他CPU裡快取了該記憶體地址的資料無效。
實際上,為了提高處理速度,CPU不會直接和記憶體通訊,而是將系統記憶體的資料讀到內部快取(L1,L2或其他後)再操作,但操作完不知道何時回寫到記憶體。如果對生命了volatile的變數進行寫操作,JVM會向CPU發出Lock字首指令,將這個變數強制寫回到系統記憶體。但是,就算寫回到系統記憶體,如果其他處理器快取的值還是舊的,再執行計算操作就會有問題。所以在多處理器下,為了保證各個處理器的快取是一致的,就會實現快取一致性協議,每個處理器都通過嗅探在總線上傳播的資料來檢查自己快取值是不是過期了,當處理器發現自己換行對應的記憶體地址唄修改,就會將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中吧資料讀到處理器快取裡。
volatile的具體兩條具體實現原則:
1. Lock字首指令會引起CPU快取寫會記憶體。lock指令確保在聲言該訊號期間,處理器可以獨佔任何共享記憶體(鎖住匯流排),但最近的處理器裡,一般不鎖匯流排,而是所快取。
2. 處理器鎖住快取或者匯流排後,將變數操作完畢後,寫回記憶體,會導致其他處理的快取無效,因為其他處理器會嗅探匯流排來確保記憶體快取資料都是真實可靠的,一旦嗅探到記憶體快取資料已經被其他CPU修改,則會下次訪問相同地址,取得最新資料。