深入理解 Volatile 的實現原理

Volatile 的官方定義

Java 語言規範第三版中對 volatile 的定義如下: java 編程語言允許線程訪問共享變量,爲了確保共享變量能被準確和一致的更新,線程應該確保通過排他鎖單獨獲得這個變量。Java 語言提供了 volatile,在某些情況下比鎖更加方便。如果一個字段被聲明成 volatile,java 線程內存模型確保所有線程看到這個變量的值是一致的。

什麼情況下可使用 volatile

  1. 在多線程併發編程時,爲了保持共享變量在多個線程的一致性。 =》 可見性
  2. 爲了保證代碼執行按編碼的順序執行。 =》 有序性

併發編程中的三個特性:原子性,有序性和可見性。volatile就作用了其中的兩個。

爲什麼使用 volatile

恰當的使用,它的使用和執行成本比synchronized更低,因爲不會引起線程上下文的切換和調度。

volatile的實現原理是什麼

volatile 是依賴於硬件層面的支持,即需要 CPU 的指定來實現。

對於volatile修飾的變量,在彙編語言層面會多一行指令 0x01a3de24: lock addl $0x0,(%esp);。而該lock指令通過查IA-32架構可知主要做兩件事 :

  1. 將當前處理器緩存行的數據寫回到系統內存中。
  2. 該寫回內存操作會引起其他CPU裏緩存了該內存地址的數據失效。 =》 CPU 的嗅探機制。

詳細原理如下:

處理器爲了提高處理速度,不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2 或其他)後再進行操作,但操作完之後不知道何時會寫到內存,如果對聲明瞭 Volatile 變量進行寫操作,JVM 就會向處理器發送一條 Lock 前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。

但是就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題,所以在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操作的時候,會強制重新從系統內存裏把數據讀到處理器緩存裏

LOCK 前綴指令的改進

Lock 前綴指令會引起處理器緩存回寫到內存。 Lock 前綴指令導致在執行指令期間,聲言處理器的 LOCK# 信號,在該信號期間,會獨佔使用任何共享內存

  1. 第一階段:

    1. LOCK指令會鎖住總線,導致其他的處理器不能訪問總線,也就不能訪問系統內存。將多線程的併發變成了串行執行。
  2. 優化後

    1. LOCK指令不再鎖總線,而是鎖緩存行。並將數據會寫到該緩存,使用緩存一致性來保證原子性

緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據。一個處理器的緩存回寫到內存會導致其他處理器的緩存無效。

新的 CPU 會使用 MESI(修改,獨佔,共享,無效)控制協議來維護內部緩存和其他處理器的緩存的一致性。

可以看出硬件技術的進步對於軟件的性能提升有質的飛越。

volatile在軟件層面的優化

併發編程大師 Doug lea在 JDK1.7 中新增了隊列集合類 LinkedTransferQueue,在使用Volatile時用追價字節的方式優化隊列出棧和入棧的性能。

爲什麼追加 64 字節能夠提高併發編程的效率呢?

因爲對於英特爾酷睿 i7,酷睿, Atom 和 NetBurst, Core Solo 和 Pentium M 處理器的 L1,L2 或 L3 緩存的高速緩存行是 64 個字節寬,不支持部分填充緩存行,這意味着如果隊列的頭節點和尾節點都不足 64 字節的話,處理器會將它們都讀到同一個高速緩存行中,在多處理器下每個處理器都會緩存同樣的頭尾節點,當一個處理器試圖修改頭接點時會將整個緩存行鎖定,那麼在緩存一致性機制的作用下,會導致其他處理器不能訪問自己高速緩存中的尾節點,而隊列的入隊和出隊操作是需要不停修改頭接點和尾節點,所以在多處理器的情況下將會嚴重影響到隊列的入隊和出隊效率。Doug lea 使用追加到 64 字節的方式來填滿高速緩衝區的緩存行,避免頭接點和尾節點加載到同一個緩存行,使得頭尾節點在修改時不會互相鎖定。

上一段話核心意思是: 隊列A的尾節點和隊列B的頭節點 在同一緩存行,隊列 B 修改頭節點時會鎖住整個緩存行,導致隊列A 不能訪問自己的尾節點。因此需要補全 64 字節,讓尾節點獨佔一個緩存行。

該方式是對空間和性能的一個折中和取巧方案。如果併發較大,修改比較頻繁,可以使用該方式。主要是爲了避免相互鎖定

那什麼情況下不適合呢?

  1. 緩存行非 64 字節寬的處理器。比較老一些的處理器,如 P6 和奔騰處理器的緩存行是 32 字節寬。
  2. 共享變量不會被頻繁的寫,因爲使用追加字節的方式需要處理器讀取更多的字節到高速緩衝區,這本身就會帶來一定的性能消耗,共享變量如果不被頻繁寫的話,鎖的機率也非常小,就沒必要通過追加字節的方式來避免相互鎖定。

感想

越底層的知識越基礎越重要。CPU 和內存,磁盤的交互機制不瞭解,就不能很好的在軟件層面利用硬件能力進行性能提升。

附上執行指令和 volatile 執行原理圖

CPU 的執行指令

volatile 執行原理圖

參考文獻

https://www.infoq.cn/article/...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章