做者:LeanCloud 後端高級工程師 郭瑞html
內容分享視頻版本: 內存屏障及其在-JVM-內的應用java
Java 爲了能在不一樣架構的 CPU 上運行,提煉出一套本身的內存模型,定義出來 Java 程序該怎麼樣和這個抽象的內存模型進行交互,定義出來程序的運行過程,什麼樣的指令能夠重排,什麼樣的不行,指令之間可見性如何等。至關因而規範出來了 Java 程序運行的基本規範。這個模型定義會很不容易,它要有足夠彈性,以適應各類不一樣的硬件架構,讓這些硬件在支持 JVM 時候都能知足運行規範;它又要足夠嚴謹,讓應用層代碼編寫者能依靠這套規範,知道程序怎麼寫才能在各類系統上運行都不會有歧義,不會有併發問題。linux
在著名的 《深刻理解 Java 虛擬機》一書的圖 12-1 指出了在 JMM 內,線程、主內存、工做內存的關係。圖片來自該書的 Kindle 版:git
從內存模型一詞就能看出來,這是對真實世界的模擬。圖中 Java 線程對應的就是 CPU,工做內存對應的就是 CPU Cache,Java 提煉出來的一套 Save、Load 指令對應的就是緩存一致性協議,就是 MESI 等協議,最後主內存對應的就是 Memory。真實世界的硬件須要根據自身狀況去向這套模型裏套。github
JMM 完善於 JSR-133,如今通常會把詳細說明放在 Java Language 的 Spec 上,好比 Java11 的話在:Chapter 17. Threads and Locks。在這些說明以外,還有個特別出名的 Cookbook,叫 The JSR-133 Cookbook for Compiler Writers。後端
JVM 按先後分別有讀、寫兩種操做以全排列方式一共提供了四種 Barrier,名稱就是左右兩邊操做的名字拼接。好比 LoadLoad
Barrier 就是放在兩次 Load 操做中間的 Barrier,LoadStore
就是放在 Load 和 Store 中間的 Barrier。Barrier 類型及其含義以下:緩存
LoadLoad
,操做序列 Load1, LoadLoad, Load2,用於保證訪問 Load2 的讀取操做必定不能重排到 Load1 以前。相似於前面說的 Read Barrier
,須要先處理 Invalidate Queue 後再讀 Load2;StoreStore
,操做序列 Store1, StoreStore, Store2,用於保證 Store1 及其以後寫出的數據必定先於 Store2 寫出,即別的 CPU 必定先看到 Store1 的數據,再看到 Store2 的數據。可能會有一次 Store Buffer 的刷寫,也可能經過全部寫操做都放入 Store Buffer 排序來保證;LoadStore
,操做序列 Load1, LoadStore, Store2,用於保證 Store2 及其以後寫出的數據被其它 CPU 看到以前,Load1 讀取的數據必定先讀入緩存。甚至可能 Store2 的操做依賴於 Load1 的當前值。這個 Barrier 的使用場景可能和上一節講的 Cache 架構模型很難對應,畢竟那是一個極簡結構,而且只是一種具體的 Cache 架構,而 JVM 的 Barrier 要足夠抽象去應付各類不一樣的 Cache 架構。若是跳出上一節的 Cache 架構來講,我理解用到這個 Barrier 的場景多是說某種 CPU 在寫 Store2 的時候,認爲刷寫 Store2 到內存,將其它 CPU 上 Store2 所在 Cache Line 設置爲無效的速度要快於從內存讀取 Load1,因此作了這種重排。StoreLoad
,操做序列 Store1, StoreLoad, Load2,用於保證 Store1 寫出的數據被其它 CPU 看到後才能讀取 Load2 的數據到緩存。若是 Store1 和 Load2 操做的是同一個地址,StoreLoad Barrier 須要保證 Load2 不能讀 Store Buffer 內的數據,得是從內存上拉取到的某個別的 CPU 修改過的值。StoreLoad
通常會認爲是最重的 Barrier 也是能實現其它全部 Barrier 功能的 Barrier。對上面四種 Barrier 解釋最好的是來自這裏:jdk/MemoryBarriers.java at 6bab0f539fba8fb441697846347597b4a0ade428 · openjdk/jdk · GitHub,感受比 JSR-133 Cookbook 裏的還要細一點。bash
爲何這一堆 Barrier 裏 StoreLoad
最重?架構
所謂的重實際就是跟內存交互次數,交互越多延遲越大,也就是越重。StoreStore
, LoadLoad
兩個都不提了,由於它倆要麼只限制讀,要麼只限制寫,也即只有一次內存交互。只有 LoadStore
和 StoreLoad
看上去有可能對讀寫都有限制。但 LoadStore
裏實際限制的更多的是讀,即 Load 數據進來,它並不對最後的 Store 存出去數據的可見性有要求,只是說 Store 不能重排到 Load 以前。而反觀 StoreLoad
,它是說不能讓 Load 重排到 Store 以前,這麼一來得要求在 Load 操做前刷寫 Store Buffer 到內存。不去刷 Store Buffer 的話,就可能致使先執行了讀取操做,以後再刷 Store Buffer 致使寫操做實際被重排到了讀以後。而數據一旦刷寫出去,別的 CPU 就能看到,看到以後可能就會修改下一步 Load 操做的內存致使 Load 操做的內存所在 Cache Line 無效。若是容許 Load 操做從一個可能被 Invalidate 的 Cache Line 裏讀數據,則表示 Load 從實際意義上來講被重排到了 Store 以前,由於這個數據多是 Store 前就在 Cache 中的,至關於讀操做提早了。爲了不這種事發生,Store 完成後必定要去處理 Invalidate Queue,去判斷本身 Load 操做的內存所在 Cache Line 是否被設置爲無效。這麼一來爲了知足 StoreLoad
的要求,一方面要刷 Store Buffer,一方面要處理 Invalidate Queue,則最差狀況下會有兩次內存操做,讀寫分別一次,因此它最重。併發
StoreLoad
爲何能實現其它 Barrier 的功能?
這個也是從前一個問題結果能看出來的。StoreLoad
由於對讀寫操做均有要求,因此它能實現其它 Barrier 的功能。其它 Barrier 都是隻對讀寫之中的一個方面有要求。
不過這四個 Barrier 只是 Java 爲了跨平臺而設計出來的,實際上根據 CPU 的不一樣,對應 CPU 平臺上的 JVM 可能能夠優化掉一些 Barrier。好比不少 CPU 在讀寫同一個變量的時候能保證它連續操做的順序性,那就不用加 Barrier 了。好比 Load x; Load x.field
讀 x 再讀 x 下面某個 field,若是訪問同一個內存 CPU 能保證順序性,兩次讀取之間的 Barrier 就再也不須要了,根據字節碼編譯獲得的彙編指令中,原本應該插入 Barrier 的地方會被替換爲 nop
,即空操做。在 x86 上,實際只有 StoreLoad
這一個 Barrier 是有效的,x86 上沒有 Invalidate Queue,每次 Store 數據又都會去 Store Buffer 排隊,因此 StoreStore
, LoadLoad
都不須要。x86 又能保證 Store 操做都會走 Store Buffer 異步刷寫,Store 不會被重排到 Load 以前,LoadStore
也是不須要的。只剩下一個 StoreLoad
Barrier 在 x86 平臺的 JVM 上被使用。
x86 上怎麼使用 Barrier 的說明能夠在 openjdk 的代碼中看到,在這裏src/hotspot/cpu/x86/assembler_x86.hpp。能夠看到 x86 下使用的是 lock
來實現 StoreLoad
,而且只有 StoreLoad
有效果。在這個代碼註釋中還大體介紹了使用 lock
的緣由。
JVM 上對 Barrier 的一個主要應用是在 volatile
關鍵字的實現上。對這個關鍵字的實現 Oracle 有這麼一段描述:
Using volatile variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What's more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change.
來自 Oracle 對 Atomic Access 的說明:Atomic Access。大體上就是說被 volatile
標記的變量須要維護兩個特性:
volatile
變量總能讀到它最新值,即最後一個對它的寫入操做,無論這個寫入是否是當前線程完成的。happens-before
關係,對 volatile
變量的寫入不能重排到寫入以前的操做以前,從而保證別的線程看到寫入值後就能知道寫入以前的操做都已經發生過;對 volatile
的讀取操做必定不能被重排到後續操做以後,好比我須要讀 volatile
後根據讀到的值作一些事情,作這些事情若是重排到了讀 volatile
以前,則至關於沒有知足讀 volatile
須要讀到最新值的要求,由於後續這些事情是根據一箇舊 volatile
值作的。須要看到兩個事情,一個是禁止指令重排不是禁止全部的重排,只是 volatile
寫入不能向前排,讀取不能向後排。別的重排仍是會容許。另外一個是禁止指令重排實際也是爲了去知足可見性而附帶產生的。因此 volatile
對上述兩個特性的維護就能靠 Barrier 來實現。
假設約定 Normal Load, Normal Store 對應的是對普通引用的修改。比如有 int a = 1;
那 a = 2;
就是 Normal Store,int b = a;
就有一次對 a 的 Normal Load。若是變量帶着 volatile
修飾,那對應的讀取和寫入操做就是 Volatile Load 或者 Volatile Store。volatile
對代碼生成的字節碼自己沒有影響,即 Java Method 生成的字節碼不管裏面操做的變量是否是 volatile
聲明的,生成的字節碼都是同樣的。volatile
在字節碼層面影響的是 Class 內 Field 的 access_flags
(參看 Java 11 The Java Virtual Machine Specification 的 4.5 節),能夠理解爲當看到一個成員變量被聲明爲 volatile
,Java 編譯器就在這個成員變量上打個標記記錄它是 volatile
的。JVM 在將字節碼編譯爲彙編時,若是遇見好比 getfield
, putfield
這些字節碼,而且發現操做的是帶着 volatile
標記的成員變量,就會在彙編指令中根據 JMM 要求插入對應的 Barrier。
根據 volatile
語義,咱們依次看下面操做次序該用什麼 Barrier,須要說明的是這裏先後兩個操做須要操做不一樣的變量:
volatile
的變量。這種很明顯是得用 StoreStore Barrier。上面四種狀況要用 Barrier 的緣由統一來講就是前面 Oracle 對 Atomic Access 的說明,寫一個 volatile
的變量被別的 CPU 看到時,須要保證寫這個變量操做以前的操做都能完成,無論前一個操做是讀仍是寫,操做的是 volatile
變量仍是不是。若是 Store 操做作了重排,排到了前一個操做以前,就會違反這個約定。因此 volatile
變量操做是在 Store 操做前面加 Barrier,而 Store 後若是是 Normal 變量就不用 Barrier 了,重不重排都無所謂:
對於 volatile
變量的讀操做,爲了知足前面提到 volatile
的兩個特性,爲了不後一個操做重排到讀 volatile
操做以前,因此對 volatile
的讀操做都是在讀後面加 Barrier:
而若是後一個操做是 Load,則不須要再用 Barrier,能隨意重排:
最後還有個特別的,前一個操做是 Volatile Store,後一個操做是 Volatile Load:
還剩下四個 Normal 的操做,都是隨意重排,沒影響:
這些使用方式和 Java 下具體操做的對應表以下:
圖中 Monitor Enter 和 Monitor Exit 分別對應着進出 synchronized
塊。Monitor Ender 和 Volatile Load 對應,使用 Barrier 的方式相同。Monitor Exit 和 Volatile Store 對應,使用 Barrier 的方式相同。
總結一下這個圖,記憶使用 Barrier 的方法很是簡單,只要是寫了 volatile
變量,爲了保證對這個變量的寫操做被其它 CPU 看到時,這個寫操做以前發生的事情也都能被別的 CPU 看到,那就須要在寫 volatile
以前加入 Barrier。避免寫操做被向前重排致使 volatile
變量已經寫入了被別的 CPU 看到了但它前面寫入過,讀過的變量卻沒有被別的 CPU 感知到。寫入變量被別的 CPU 感知到好說,這裏讀變量怎麼可能被別的 CPU 感知到呢?主要是
在讀方面,只要是讀了 volatile
變量,爲了保證後續基於此次讀操做而執行的操做能真的根據讀到的最新值作接下來的事情,須要在讀操做以後加 Barrier。
在此以外加一個特殊的 Volatile Store, Volatile Load,爲了保證後一個讀取能看到由於前一次寫入致使的變化,因此須要加入 StoreLoad
Barrier。
JMM 說明中,除了上面表中講的 volatile
變量相關的使用 Barrier 地方以外,還有個特殊地方也會用到 Barrier,是 final
修飾符。在修改 final
變量和修改別的共享變量之間,要有一個 StoreStore
Barrier。例如 x.finalField = v; StoreStore; sharedRef = x;
下面是一組操做舉例,看具體什麼樣的變量被讀取、寫入的時候須要使用 Barrier。
最後能夠看一下 JSR-133 Cookbook 裏給的例子,大概感覺一下操做各類類型變量時候 Barrier 是怎麼加的:
總結來講,volatile
可見性包括兩個方面:
volatile
變量在寫完以後能被別的 CPU 在下一次讀取中讀取到;volatile
變量以前的操做在別的 CPU 看到 volatile
的最新值後必定也能被看到;對於第一個方面,主要經過:
volatile
變量不能使用寄存器,每次讀取都要去內存拿volatile
變量後續操做被重排到讀 volatile
以前對於第二個方面,主要是經過寫 volatile
變量時的 Barrier 保證寫 volatile
以前的操做先於寫 volatile
變量以前發生。
最後還一個特殊的,若是能用到 StoreLoad
Barrier,寫 volatile
後通常會觸發 Store Buffer 的刷寫,因此寫操做能「當即」被別的 CPU 看到。
通常提到 volatile
可見性怎麼實現,最常聽到的解釋是「寫入數據以後加一個寫 Barrier 去刷緩存到主存,讀數據以前加入 Barrier 去強制從主存讀」。
從前面對 JMM 的介紹能看到,至少從 JMM 的角度來講,這個說法是不夠準確的。一方面 Barrier 按說加在寫 volatile
變量以前,不應以後加 Barrier。而讀 volatile
是在以後會加 Barrier,而不在以前。另外一方面關於 「刷緩存」的表述也不夠準確,即便是 StoreLoad
Barrier 刷的也是 Store Buffer 到緩存裏,而不是緩存向主存去刷。若是待寫入的目標內存在當前 CPU Cache,即便觸發 Store Buffer 刷寫也是寫數據到 Cache,並不會觸發 Cache 的 Writeback 即向內存作同步的事情,同步主存也沒有意義由於別的 CPU 並不必定關心這個值;同理,即便讀 volatile
變量後有 Barrier 的存在,若是目標內存在當前 CPU Cache 且處於 Valid 狀態,那讀取操做就當即從 Cache 讀,並不會真的再去內存拉一遍數據。
須要補充的是不管是volatile
仍是普通變量在讀寫操做自己方面徹底是同樣的,即讀寫操做都交給 Cache,Cache 經過 MESI 及其變種協議去作緩存一致性維護。這兩種變量的區別就只在於 Barrier 的使用上。
在 x86 下由於除了 StoreLoad
以外其它 Barrier 都是空操做,可是讀 volatile
變量並非徹底無開銷,一方面 Java 的編譯器仍是會遵守 JMM 要求在本該加入 Barrier 的彙編指令處填入 nop
,這會阻礙 Java 編譯器的一些優化措施。好比原本能進行指令重排的不敢進行指令重排等。另外由於訪問的變量被聲明爲 volatile
,每次讀取它都得從內存( 或 Cache ) 要,而不能把 volatile
變量放入寄存器反覆使用。這也下降了訪問變量的性能。
理想狀況下對 volatile
字段的使用應當多讀少寫,而且儘可能只有一個線程進行寫操做。不過讀 volatile
相對讀普通變量來講也有開銷存在,只是通常不是特別重。
[[CPU Cache 基礎]] 這篇文章內介紹了 False Sharing 的概念以及如何觀察到 False Sharing 現象。其中有個關鍵點是爲了能更好的觀察到 False Sharing,得將被線程操做的變量聲明爲 volatile
,這樣 False Sharing 出現時性能降低會很是多,但若是去掉 volatile
性能降低比率就會減小,這是爲何呢?
簡單來講若是沒有 volatile
聲明,也即沒有 Barrier 存在,每次對變量進行修改若是當前變量所在內存的 Cache Line 不在當前 CPU,那就將修改的操做放在 Store Buffer 內等待目標 Cache Line 加載後再實際執行寫入操做,這至關於寫入操做在 Store Buffer 內作了積累,好比 a++
操做不是每次執行都會向 Cache 裏執行加一,而是在 Cache 加載後直接執行好比加 10,加 100,從而將一批加一操做合併成一次 Cache Line 寫入操做。而有了 volatile
聲明,有了 Barrier,爲了保證寫入數據的可見性,就會引入等待 Store Buffer 刷寫 Cache Line 的開銷。當目標 Cache Line 還未加載入當前 CPU 的 Cache,寫數據先寫 Store Buffer,但看到例如 StoreLoad
Barrier 後須要等待 Store Buffer 的刷寫才能繼續執行下一條指令。仍是拿 a++
來講,每次加一操做再也不能積累,而是必須等着 Cache Line 加載,執行完 Store Buffer 刷寫後才能繼續下一個寫入,這就放大了 Cache Miss 時的影響,因此出現 False Sharing 時 Cache Line 在多個 CPU 之間來回跳轉,在被修改的變量有了 volatile
聲明後會執行的更慢。
再進一步說,我是在我本機作測試,個人機器是 x86 架構的,在個人機器上實際只有 StoreLoad
Barrier 會真的起做用。咱們去 open jdk 的代碼裏看看 StoreLoad
Barrier 是怎麼加上的。
先看這裏,JSR-133 Cookbook 裏定義了一堆 Barrier,但 JVM 虛擬機上實際還會定義更多一些 Barrier 在 src/hotspot/share/runtime/orderAccess.hpp。
每一個不一樣的系統或 CPU 架構會使用不一樣的 orderAccess
的實現,好比 linux x86 的在 src/hotspot/os_cpu/linux_x86/orderAccess_linux_x86.hpp,BSD x86 和 Linux x86 的相似在 src/hotspot/os_cpu/bsd_x86/orderAccess_bsd_x86.hpp,都是這樣定義的:
inline void OrderAccess::loadload() { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore() { compiler_barrier(); }
inline void OrderAccess::storeload() { fence(); }
inline void OrderAccess::acquire() { compiler_barrier(); }
inline void OrderAccess::release() { compiler_barrier(); }
inline void OrderAccess::fence() {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
compiler_barrier();
}
複製代碼
compiler_barrier()
只是爲了避免作指令重排,可是對應的是空操做。看到上面只有 StoreLoad
是實際有效的,對應的是 fence()
,看到 fence()
的實現是用 lock
。爲啥用 lock
在前面貼的 assembler_x86
的註釋裏有說明。
以後 volatile
變量在每次修改後,都須要使用 StoreLoad
Barrier,在解釋執行字節碼的代碼裏能看到。src/hotspot/share/interpreter/bytecodeInterpreter.cpp,看到是執行 putfield
的時候若是操做的是 volatile
變量,就在寫完以後加一個 StoreLoad
Barrier。咱們還能找到 MonitorExit
至關於對 volatile
的寫入,在 JSR-133 Cookbook 裏有說過,在 openjdk 的代碼裏也能找到證據在 src/hotspot/share/runtime/objectMonitor.cpp。
JSR-133 Cookbook 還提到 final
字段在初始化後須要有 StoreStore
Barrier,在 src/hotspot/share/interpreter/bytecodeInterpreter.cpp 也能找到。
這裏問題又來了,按 JSR-133 Cookbook 上給的圖,連續兩次 volatile
變量寫入中間不應用的是 StoreStore
嗎,從上面代碼看用的怎麼是 StoreLoad
。從 JSR-133 Cookbook 上給的 StoreLoad
是說 Store1; StoreLoad; Load2
含義是 Barrier 後面的全部讀取操做都不能重排在 Store1 前面,並非僅指緊跟着 Store1 後面的那次讀,而是無論隔多遠只要有讀取都不能作重排。因此我理解拿 volatile
修飾的變量來講,寫完 volatile
以後,程序總有某個位置會去讀這個 volatile
變量,因此寫完 volatile
變量後必定總對應着 StoreLoad
Barrier,只是理論上存在好比只寫 volatile
變量但歷來不讀它,這時候纔可能產生 StoreStore
Barrier。固然這個只是我從 JDK 代碼上和實際測試中獲得的結論。
怎麼觀察到上面說的內容是否是真的呢?咱們須要把 JDK 編碼結果打印出來。能夠參考這篇文章。簡單來講有兩個關鍵點:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
JAVA_HOME
的 jre/lib
下面若是缺乏 hsdis
則會在啓動程序時候看到:
Could not load hsdis-amd64.dylib; library not loadable; PrintAssembly is disabled
複製代碼
以後咱們去打印以前測試 False Sharing 例子中代碼編譯出來的結果,能夠看到彙編指令中,每次執行完寫 volatile
的 valueA
或者 valueB
後面都跟着 lock
指令,即便 JIT 介入後依然如此,彙編指令大體上相似於:
0x0000000110f9b180: lock addl $0x0,(%rsp) ;*putfield valueA
; - cn.leancloud.filter.service.SomeClassBench::testA@2 (line 22)
複製代碼
跟 Barrier 相關的還一個有意思的,是 Atomic 下的 LazySet 操做。拿最多見的 AtomicInteger
爲例,裏面的狀態 value
是個 volatile
的 int
,普通的 set
就是將這個狀態修改成目標值,修改後由於有 Barrier 的關係會讓其它 CPU 可見。而 lazySet
與 set
對比是這樣:
public final void set(int newValue) {
value = newValue;
}
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
複製代碼
對於 unsafe.putOrderedInt()
的內容 Java 徹底沒給出解釋,但從添加 lazySet()
這個功能的地方: Bug ID: JDK-6275329 Add lazySet methods to atomic classes,能看出來其做用是在寫入 volatile
狀態前增長 StoreStore
Barrier。它只保證本次寫入不會重排到前面寫入以前,但本次寫入何時能刷寫到內存是不作要求的,從而是一次輕量級的寫入操做,在特定場景能優化性能。
簡單介紹一下這個黑科技。好比如今有 a b c d 四個 volatile
變量,若是無腦執行:
a = 1;
b = 2;
c = 3;
d = 4;
複製代碼
會在每一個語句中間加上 Barrier。直接上面這樣寫可能還好,都是 StoreStore
的 Barrier,但若是寫 volatile
以後又有一些讀 volatile
操做,可能 Barrier 就會提高至最重的 StoreLoad
Barrier,開銷就會很大。而若是對開始的 a b c 寫入都是用寫普通變量的方式寫入,只對最後的 d 用 volatile
方式更新,即只在 d = 4
前帶上寫 Barrier,保證 d = 4
被其它 CPU 看見時,a、b、c 的值也能被別的 CPU 看見。這麼一來就能減小 Barrier 的數量,提升性能。
JVM 裏上一節介紹的 unsafe
下還有個叫 putObject
的方法,用來將一個 volatile
變量以普通變量方式更新,即不使用 Barrier。用這個 putObject
就能作到上面提到的優化。
ConcurrentLinkedQueue
是 Java 標準庫提供的無鎖隊列,它裏面就用到了這個黑科技。由於是鏈表,因此裏面有個叫 Node
的類用來存放數據,Node
連起來就構成鏈表。Node
內有個被 volatile
修飾的變量指向 Node
存放的數據。Node
的部分代碼以下:
private static class Node<E> {
volatile E item;
volatile Node<E> next;
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
....
}
複製代碼
由於 Node 被構造出來後它得經過 cas
操做隊尾 Node
的 next
引用接入鏈表,接入成功以後才須要被其它 CPU 看到,在 Node
剛構造出來的時候,Node
內的 item
實際不會被任何別的線程訪問,因此看到 Node
的構造函數能夠直接用 putObject
更新 item
,等後續 cas
操做隊列隊尾 Node
的 next
時候再以 volatile
方式更新 next
,從而帶上 Barrier,更新完成後 next
的更新包括 Node
內 item
的更新就都被別的 CPU 看到了。從而減小操做 volatile
變量的開銷。