偽共享 (圖解)
-
瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高併發實戰》 面試必備 + 面試必備 + 面試必備 【部落格園總入口 】
-
瘋狂創客圈 經典圖書 : 《SpringCloud、Nginx高併發核心程式設計》 大廠必備 + 大廠必備 + 大廠必備 【部落格園總入口 】
-
入大廠+漲工資必備: 高併發【 億級流量IM實戰】 實戰系列 【 SpringCloud Nginx秒殺】 實戰系列 【部落格園總入口 】
無鎖程式設計(Lock Free)框架 系列文章:
-
1 disruptor 使用和原理 圖解
-
2 akka 使用和原理 圖解
-
3 camel 使用和 原理 圖解
在介紹 無鎖框架 disruptor 之前,作為前置的知識,首先需要了解 偽共享問題。
1 CPU的結構
下圖是計算的基本結構。L1、L2、L3分別表示一級快取、二級快取、三級快取,越靠近CPU的快取,速度越快,容量也越小。所以L1快取很小但很快,並且緊靠著在使用它的CPU核心;L2大一些,也慢一些,並且仍然只能被一個單獨的CPU核使用;L3更大、更慢,並且被單個插槽上的所有CPU核共享;最後是主存,由全部插槽上的所有CPU核共享。
級別越小的快取,越接近CPU, 意味著速度越快且容量越少。
L1是最接近CPU的,它容量最小,速度最快,每個核上都有一個L1 Cache(準確地說每個核上有兩個L1 Cache, 一個存資料 L1d Cache, 一個存指令 L1i Cache);
L2 Cache 更大一些,例如256K,速度要慢一些,一般情況下每個核上都有一個獨立的L2 Cache;二級快取就是一級快取的緩衝器:一級快取製造成本很高因此它的容量有限,二級快取的作用就是儲存那些CPU處理時需要用到、一級快取又無法儲存的資料。
L3 Cache是三級快取中最大的一級,例如12MB,同時也是最慢的一級,在同一個CPU插槽之間的核共享一個L3 Cache。三級快取和記憶體可以看作是二級快取的緩衝器,它們的容量遞增,但單位制造成本卻遞減。
L3 Cache和L1,L2 Cache有著本質的區別。,L1和L2 Cache都是每個CPU core獨立擁有一個,而L3 Cache是幾個Cores共享的,可以認為是一個更小但是更快的記憶體。
2 快取行 cache line
快取,是由快取行Cache Line組成的。一般一行快取行有64位元組。所以使用快取時,並不是一個一個位元組使用,而是一行快取行、一行快取行這樣使用;換句話說,CPU存取快取都是按照一行,為最小單位操作的。
Cache Line可以簡單的理解為CPU Cache中的最小快取單位。目前主流的CPU Cache的Cache Line大小都是64Bytes。假設我們有一個512位元組的一級快取,那麼按照64B的快取單位大小來算,這個一級快取所能存放的快取個數就是512/64 = 8個。
3偽共享(False Sharing)問題(全網最清晰的解答)
CPU的快取系統是以快取行(cache line)為單位儲存的,一般的大小為64bytes。在多執行緒程式的執行過程中,存在著一種情況,多個需要頻繁修改的變數存在同一個快取行當中。
假設:有兩個執行緒分別訪問並修改X和Y這兩個變數,X和Y恰好在同一個快取行上,這兩個執行緒分別在不同的CPU上執行。那麼每個CPU分別更新好X和Y時將快取行刷入記憶體時,發現有別的修改了各自快取行內的資料,這時快取行會失效,從L3中重新獲取。這樣的話,程式執行效率明顯下降。為了減少這種情況的發生,其實就是避免X和Y在同一個快取行中,可以主動新增一些無關變數將快取行填充滿,比如在X物件中新增一些變數,讓它有64 Byte那麼大,正好佔滿一個快取行。
兩個執行緒(Thread1 和 Thread2)同時修改一個同一個快取行上的資料 X Y:
如果執行緒1打算更改a的值,而執行緒2準備更改b的值:
Thread1:x=3;
Thread2:y=2;
由x值被更新了,所以x值需要線上程1和執行緒2之間傳遞(從執行緒1到執行緒2),x的變更會引起整塊64bytes被交換,因為cpu核之間以cache lines的形式交換資料(cache lines的大小一般為64bytes)。
每個執行緒在不同的核中被處理。x,y是兩個頻繁修改的變數,x,y,還位於同一個快取行.
如果,CPU1修改了變數x時,L3中的快取行資料就失效了,也就是CPU2中的快取行資料也失效了,CPU2需要的y需要重新從記憶體載入。
如果,CPU2修改了變數y時,L3中的快取行資料就失效了,也就是CPU1中的快取行資料也失效了,CPU1需要的x需要重新從記憶體載入。
x,y在兩個cpu上進行修改,本來應該是互不影響的,但是由於快取行在一起,導致了相互受到了影響。
偽共享問題(False Sharing) 全網最清晰的解答:
對快取行中的單個變數進行修改了,導致整個快取行資料也就失效了,並且,會導致其他CPU的含有被改動的共享變數的快取行也失效了,就是所謂的偽共享問題。
而快取行是為了解決快取一致性的問題,所引入的。所以,解決快取一致性的問題,又導致了偽共享問題的出現。
4偽共享問題 的解決方案
簡單的說,就是 以空間換時間: 使用佔位資料,將變數的所在的 緩衝行 塞滿。 disruptor 無鎖框架就是這麼幹的。
5緩衝行 塞滿的例子
下面是一個填充了的快取行的,嘗試 p1, p2, p3, p4, p5, p6為AtomicLong的value的快取行佔位,將AtomicLong的value變數的所在的 緩衝行 塞滿,
程式碼如下:
package com.baidu.fsg.uid.utils;
//...省略import
public class PaddedAtomicLong extends AtomicLong {
private static final long serialVersionUID = -3415778863941386253L;
```
/** Padded 6 long (48 bytes) */
public volatile long p1, p2, p3, p4, p5, p6 = 7L;
/**
* Constructors from {@link AtomicLong}
*/
public PaddedAtomicLong() {
super();
}
public PaddedAtomicLong(long initialValue) {
super(initialValue);
}
/**
* To prevent GC optimizations for cleaning unused padded references
*/
public long sumPaddingToPreventOptimization() {
return p1 + p2 + p3 + p4 + p5 + p6;
}
```
}
例子的部分結果如下:
printable = com.crazymakercircle.basic.demo.cas.busi.PaddedAtomicLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 50 08 01 f8 (01010000 00001000 00000001 11111000) (-134150064)
12 4 (alignment/padding gap)
16 8 long AtomicLong.value 0
24 8 long PaddedAtomicLong.p1 0
32 8 long PaddedAtomicLong.p2 0
40 8 long PaddedAtomicLong.p3 0
48 8 long PaddedAtomicLong.p4 0
56 8 long PaddedAtomicLong.p5 0
64 8 long PaddedAtomicLong.p6 7
Instance size: 72 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
6 偽共享False Sharing在java6/7中解決方案
如何避免False Sharing在java 6 7 8 中有不同的實現方式, 這篇文章對比了在6/7/8下面的實現。國內的多篇關於偽共享的文章基本都來源於Martin的兩篇部落格。
部落格1和部落格2,部落格1主要介紹了什麼是False Sharing以及怎麼避免False Sharing(在java6的環境下),我在看完這篇文文章後使用他的testbench進行了測試,得到的結果是在java6環境下,使用6個long變數進行填充是不一定能完全避免false sharing,但是我使用了
public final static class VolatileLong {
public volatile long q1, q2, q3, q4, q5, q6, q7;
public volatile long value = 0L;
public volatile long p1, p2, p3, p4, p5, p6, p7;
}
說明:前面的例子,借鑑了這個方案。
這種方式得到的結果是完全能夠避免false sharing,我以此郵件了作者Martin Thompson說明此問題,Martin Thompson很快回了郵件附上了部落格2的連結問我是否看過部落格2的內容,我讀過之後發現部落格2寫的是在java7的環境下虛擬機器層面會對沒有使用的變數進行優化,所以會導致false sharing的問題,我覺得這是一個新的問題並不能解釋我在java6環境下發生的現象。在java7環境下要使用填充的方式避免false sharing需要繞很多彎彎而且並不一定能夠達到效果。所以我覺得我們不能通過這種“黑科技”解決false sharing的問題,包括Martin Thompson的很多人都希望jvm的開發團隊能夠搞出一套機制能夠支援在上層決定多個欄位是否可以出現在同一個cache line,所以應大家的響應,在java8中,jvm團隊搞出了@Contended註解來進行支援
java8中解決偽共享
關於@Contended的用法,我們可以參考一個連結,這是jvm團隊內部關於JEP-142實現的一個郵件回覆,雖然可能和具體實現有所差別,但是參考價值很大。所以LongAdder在java8中的實現已經採用了@Contended
比起引入填充欄位,一個更加簡單有效的方式是在你需要避免“false sharing”的欄位上標記註解,這可以暗示虛擬機器“這個欄位可以分離到不同的cache line中”,這是JEP 142的目標。
JEP引入了 @Contended 註解。在JAVA 8中,快取行填充終於被JAVA原生支援了。JAVA 8中添加了一個@Contended的註解,新增這個的註解,將會在自動進行快取行填充。以上的例子可以改為:
package com.crazymakercircle.basic.demo.cas.busi;
import sun.misc.Contended;
public class ContendedDemo
{
//有填充的演示成員
@Contended
public volatile long padVar;
//沒有填充的演示成員
public volatile long notPadVar;
}
以上程式碼使得x和y都在不同的cache line中。@Contended 使得y欄位遠離了物件頭部分。
printable = com.crazymakercircle.basic.demo.cas.busi.ContendedDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 50 08 01 f8 (01010000 00001000 00000001 11111000) (-134150064)
12 4 (alignment/padding gap)
16 8 long ContendedDemo.padVar 0
24 8 long ContendedDemo.notPadVar 0
Instance size: 32 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
執行時,必須加上虛擬機器引數-XX:-RestrictContended,@Contended註釋才會生效。很多文章把這個漏掉了,那樣的話實際上就沒有起作用。
新的結果;
printable = com.crazymakercircle.basic.demo.cas.busi.ContendedDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 50 08 01 f8 (01010000 00001000 00000001 11111000) (-134150064)
12 4 (alignment/padding gap)
16 8 long ContendedDemo.notPadVar 0
24 128 (alignment/padding gap)
152 8 long ContendedDemo.padVar 0
160 128 (loss due to the next object alignment)
Instance size: 288 bytes
Space losses: 132 bytes internal + 128 bytes external = 260 bytes total
@Contended註釋還可以新增在類上,每一個成員,都會加上。
可見至少在JDK1.8以上環境下, 只有@Contended註解才能解決偽共享問題, 但是消耗也很大, 佔用了寶貴的快取, 用的時候要謹慎。
對於偽共享,我們在實際開發中該怎麼做?
通過上面大篇幅的介紹,我們已經知道偽共享的對程式的影響。那麼,在實際的生產開發過程中,我們一定要通過快取行填充去解決掉潛在的偽共享問題嗎?
其實並不一定。
首先就是多次強調的,偽共享是很隱蔽的,我們暫時無法從系統層面上通過工具來探測偽共享事件。其次,不同型別的計算機具有不同的微架構(如 32 位系統和 64 位系統的 java 物件所佔自己數就不一樣),如果設計到跨平臺的設計,那就更難以把握了,一個確切的填充方案只適用於一個特定的作業系統。還有,快取的資源是有限的,如果填充會浪費珍貴的 cache 資源,並不適合大範圍應用。最後,目前主流的 Intel 微架構 CPU 的 L1 快取,已能夠達到 80% 以上的命中率。
綜上所述,並不是每個系統都適合花大量精力去解決潛在的偽共享問題
回到◀瘋狂創客圈▶
瘋狂創客圈 - Java高併發研習社群,為大家開啟大廠之門