因此醋寝,對于多個線程共享的熱點數(shù)據(jù)搞挣,即經(jīng)常會修改的數(shù)據(jù)带迟,應(yīng)該避免這些數(shù)據(jù)剛好在同一個 Cache Line 中音羞,否則就會出現(xiàn)為偽共享的問題。
接下來仓犬,看看在實際項目中是用什么方式來避免偽共享的問題的。
在 Linux 內(nèi)核中存在?__cacheline_aligned_in_smp?宏定義,是用于解決偽共享的問題侦讨。
從上面的宏定義,我們可以看到:
如果在多核(MP)系統(tǒng)里翠语,該宏定義是?__cacheline_aligned,也就是 Cache Line 的大胁票摺肌括;
而如果在單核系統(tǒng)里,該宏定義是空的酣难;
因此谍夭,針對在同一個 Cache Line 中的共享的數(shù)據(jù),如果在多核之間競爭比較嚴(yán)重憨募,為了防止偽共享現(xiàn)象的發(fā)生紧索,可以采用上面的宏定義使得變量在 Cache Line 里是對齊的。
舉個例子菜谣,有下面這個結(jié)構(gòu)體:
結(jié)構(gòu)體里的兩個成員變量 a 和 b 在物理內(nèi)存地址上是連續(xù)的珠漂,于是它們可能會位于同一個 Cache Line 中,如下圖:
所以尾膊,為了防止前面提到的 Cache 偽共享問題媳危,我們可以使用上面介紹的宏定義,將 b 的地址設(shè)置為 Cache Line 對齊地址冈敛,如下:
這樣 a 和 b 變量就不會在同一個 Cache Line 中了
所以济舆,避免 Cache 偽共享實際上是用空間換時間的思想,浪費一部分 Cache 空間莺债,從而換來性能的提升滋觉。
我們再來看一個應(yīng)用層面的規(guī)避方案,有一個 Java 并發(fā)框架 Disruptor 使用「字節(jié)填充 + 繼承」的方式齐邦,來避免偽共享的問題椎侠。
Disruptor 中有一個 RingBuffer 類會經(jīng)常被多個線程使用
你可能會覺得 RingBufferPad 類里 7 個 long 類型的名字很奇怪,但事實上措拇,它們雖然看起來毫無作用我纪,但卻對性能的提升起到了至關(guān)重要的作用。
我們都知道丐吓,CPU Cache 從內(nèi)存讀取數(shù)據(jù)的單位是 CPU Cache Line浅悉,一般 64 位 CPU 的 CPU Cache Line 的大小是 64 個字節(jié),一個 long 類型的數(shù)據(jù)是 8 個字節(jié)券犁,所以 CPU 一下會加載 8 個 long 類型的數(shù)據(jù)术健。
根據(jù) JVM 對象繼承關(guān)系中父類成員和子類成員,內(nèi)存地址是連續(xù)排列布局的粘衬,因此 RingBufferPad 中的 7 個 long 類型數(shù)據(jù)作為 Cache Line?前置填充荞估,而 RingBuffer 中的 7 個 long 類型數(shù)據(jù)則作為 Cache Line?后置填充咳促,這 14 個 long 變量沒有任何實際用途,更不會對它們進(jìn)行讀寫操作勘伺。
另外跪腹,RingBufferFelds 里面定義的這些變量都是?final?修飾的,意味著第一次加載之后不會再修改飞醉, 又由于「前后」各填充了 7 個不會被讀寫的 long 類型變量冲茸,所以無論怎么加載 Cache Line,這整個 Cache Line 里都沒有會發(fā)生更新操作的數(shù)據(jù)缅帘,于是只要數(shù)據(jù)被頻繁地讀取訪問噪裕,就自然沒有數(shù)據(jù)被換出 Cache 的可能,也因此不會產(chǎn)生偽共享的問題股毫。