cpu緩存系統中是以緩存行(cache line)為機關存儲的。目前主流的cpu cache的cache line大小都是64bytes。在多線程情況下,如果需要修改“共享同一個緩存行的變量”,就會無意中影響彼此的性能,這就是僞共享(false sharing)。
由于cpu的速度遠遠大于記憶體速度,是以cpu設計者們就給cpu加上了緩存(cpu cache)。 以免運算被記憶體速度拖累。(就像我們寫代碼把共享資料做cache不想被db存取速度拖累一樣),cpu cache分成了三個級别:l1,l2,l3。級别越小越接近cpu, 是以速度也更快, 同時也代表着容量越小。
cpu擷取資料回依次從l1,l2,l3中查找,如果都找不到則會直接向記憶體查找。
由于共享變量在cpu緩存中的存儲是以緩存行為機關,一個緩存行可以存儲多個變量(存滿目前緩存行的位元組數);而cpu對緩存的修改又是以緩存行為最小機關的,那麼就會出現上訴的僞共享問題。
cache line可以簡單的了解為cpu cache中的最小緩存機關,今天的cpu不再是按位元組通路記憶體,而是以64位元組為機關的塊(chunk)拿取,稱為一個緩存行(cache line)。當你讀一個特定的記憶體位址,整個緩存行将從主存換入緩存,并且通路同一個緩存行内的其它值的開銷是很小的。
看如下代碼示例:
表面上看,第二個循環工作量為第一個循環的1/16;但是執行時間是相差不大的,假設在記憶體規整的情況下,每16個int 占用4*16=64位元組,正好一個緩存行,也就是說這兩個循環通路記憶體的次數是一緻的。導緻耗時相差不大。
目前常用的緩存設計是n路組關聯(n-way set associative cache),他的原理是把一個緩存按照n個cache line作為一組(set),緩存按組劃為等分。每個記憶體塊能夠被映射到相對應的set中的任意一個緩存行中。比如一個16路緩存,16個cache line作為一個set,每個記憶體塊能夠被映射到相對應的set
中的16個cacheline中的任意一個。一般地,具有一定相同低bit位位址的記憶體塊将共享同一個set。
下圖為一個2-way的cache。由圖中可以看到main memory中的index0,2,4都映射在way0的不同cacheline中,index1,3,5都映射在way1的不同cacheline中。

多核cpu都有自己的專有緩存(一般為l1,l2),以及同一個cpu插槽之間的核共享的緩存(一般為l3)。不同核心的cpu緩存中難免會加載同樣的資料,那麼如何保證資料的一緻性呢,就是mesi協定了。
在mesi協定中,每個cache line有4個狀态,可用2個bit表示,它們分别是:
m(modified):這行資料有效,資料被修改了,和記憶體中的資料不一緻,資料隻存在于本cache中;
e(exclusive):這行資料有效,資料和記憶體中的資料一緻,資料隻存在于本cache中;
s(shared):這行資料有效,資料和記憶體中的資料一緻,資料存在于很多cache中;
i(invalid):這行資料無效。
那麼,假設有一個變量i=3(應該是包括變量i的緩存塊,塊大小為緩存行大小);已經加載到多核(a,b,c)的緩存中,此時該緩存行的狀态為s;此時其中的一個核a改變了變量i的值,那麼在核a中的目前緩存行的狀态将變為m,b,c核中的目前緩存行狀态将變為i。如下圖:
那麼為什麼會出現僞共享問題呢?上訴的情況再擴充一下,假設在多線程情況下,x,y兩個共享變量在同一個緩存行中,核a修改變量x,會導緻核b,核c中的x變量和y變量同時失效。
此時對于在核a上運作的線程,僅僅隻是修改了了變量x,卻導緻同一個緩存行中的所有變量都無效,需要重新刷緩存(并不一定代表每次都要從記憶體中重新載入,也有可能是從其他cache中導入資料,具體的實作要看各個晶片廠商的實作了)。
假設此時在核b上運作的線程,正好想要修改變量y,那麼就會出現互相競争,互相失效的情況,這就是僞共享啦。
執行結果:
現在,我們将volatilelong中不使用的6個long變量注釋掉,再次執行:
可以看到,兩個程式邏輯完全一緻,隻是注釋掉了幾個沒有使用到的變量,卻導緻性能相差很大。 我們知道一條緩存行有64位元組, 而java程式的對象頭固定占8位元組(32位系統)或12位元組(64位系統預設開啟壓縮, 不開壓縮為16位元組). 我們隻需要填6個無用的長整型補上6*8=48位元組, 讓不同的volatilelong對象處于不同的緩存行, 就可以避免僞共享了(64位系統超過緩存行的64位元組也無所謂,隻要保證不同線程不要操作同一緩存行就可以)。這個辦法叫做補齊(padding)。
java8中已經提供了官方的解決方案,java8中新增了一個注解:@sun.misc.contended。加上這個注解的類會自動補齊緩存行,需要注意的是此注解預設是無效的,需要在jvm啟動時設定-xx:-restrictcontended才會生效。
運作結果: