(1)什麼是 CPU 緩存行?java
(2)什麼是內存屏障?git
(3)什麼是僞共享?數組
(4)如何避免僞共享?緩存
CPU 是計算機的心臟,全部運算和程序最終都要由它來執行。數據結構
主內存(RAM)是數據存放的地方,CPU 和主內存之間有好幾級緩存,由於即便直接訪問主內存也是很是慢的。多線程
若是對一塊數據作相同的運算屢次,那麼在執行運算的時候把它加載到離 CPU 很近的地方就有意義了,好比一個循環計數,你不想每次循環都跑到主內存去取這個數據來增加它吧。架構
越靠近 CPU 的緩存越快也越小。jvm
因此 L1 緩存很小但很快,而且緊靠着在使用它的 CPU 內核。源碼分析
L2 大一些,也慢一些,而且仍然只能被一個單獨的 CPU 核使用。性能
L3 在現代多核機器中更廣泛,仍然更大,更慢,而且被單個插槽上的全部 CPU 核共享。
最後,主存保存着程序運行的全部數據,它更大,更慢,由所有插槽上的全部 CPU 核共享。
當 CPU 執行運算的時候,它先去 L1 查找所需的數據,再去 L2,而後是 L3,最後若是這些緩存中都沒有,所需的數據就要去主內存拿。
走得越遠,運算耗費的時間就越長。
因此若是進行一些很頻繁的運算,要確保數據在 L1 緩存中。
緩存是由緩存行組成的,一般是 64 字節(經常使用處理器的緩存行是 64 字節的,比較舊的處理器緩存行是 32 字節),而且它有效地引用主內存中的一塊地址。
一個 Java 的 long 類型是 8 字節,所以在一個緩存行中能夠存 8 個 long 類型的變量。
在程序運行的過程當中,緩存每次更新都從主內存中加載連續的 64 個字節。所以,若是訪問一個 long 類型的數組時,當數組中的一個值被加載到緩存中時,另外 7 個元素也會被加載到緩存中。
可是,若是使用的數據結構中的項在內存中不是彼此相鄰的,好比鏈表,那麼將得不到免費緩存加載帶來的好處。
不過,這種免費加載也有一個壞處。設想若是咱們有個 long 類型的變量 a,它不是數組的一部分,而是一個單獨的變量,而且還有另一個 long 類型的變量 b 緊挨着它,那麼當加載 a 的時候將免費加載 b。
看起來彷佛沒有什麼毛病,可是若是一個 CPU 核心的線程在對 a 進行修改,另外一個 CPU 核心的線程卻在對 b 進行讀取。
當前者修改 a 時,會把 a 和 b 同時加載到前者核心的緩存行中,更新完 a 後其它全部包含 a 的緩存行都將失效,由於其它緩存中的 a 不是最新值了。
而當後者讀取 b 時,發現這個緩存行已經失效了,須要從主內存中從新加載。
請記住,咱們的緩存都是以緩存行做爲一個單位來處理的,因此失效 a 的緩存的同時,也會把 b 失效,反之亦然。
這樣就出現了一個問題,b 和 a 徹底不相干,每次卻要由於 a 的更新須要從主內存從新讀取,它被緩存未命中給拖慢了。
這就是傳說中的僞共享。
好了,上面介紹完CPU的緩存架構及緩存行機制,下面進入咱們的正題——僞共享。
當多線程修改互相獨立的變量時,若是這些變量共享同一個緩存行,就會無心中影響彼此的性能,這就是僞共享。
咱們來看看下面這個例子,充分說明了僞共享是怎麼回事。
public class FalseSharingTest {
public static void main(String[] args) throws InterruptedException {
testPointer(new Pointer());
}
private static void testPointer(Pointer pointer) throws InterruptedException {
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
pointer.x++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
pointer.y++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(System.currentTimeMillis() - start);
System.out.println(pointer);
}
}
class Pointer {
volatile long x;
volatile long y;
}
複製代碼
這個例子中,咱們聲明瞭一個 Pointer 的類,它包含 x 和 y 兩個變量(必須聲明爲volatile,保證可見性,關於內存屏障的東西咱們後面再講),一個線程對 x 進行自增1億次,一個線程對 y 進行自增1億次。
能夠看到,x 和 y 徹底沒有任何關係,可是更新 x 的時候會把其它包含 x 的緩存行失效,同時也就失效了 y,運行這段程序輸出的時間爲3890ms
。
僞共享的原理咱們知道了,一個緩存行是 64 個字節,一個 long 類型是 8 個字節,因此避免僞共享也很簡單,筆者總結了下大概有如下三種方式:
(1)在兩個 long 類型的變量之間再加 7 個 long 類型
咱們把上面的Pointer改爲下面這個結構:
class Pointer {
volatile long x;
long p1, p2, p3, p4, p5, p6, p7;
volatile long y;
}
複製代碼
再次運行程序,會發現輸出時間神奇的縮短爲了695ms
。
(2)從新建立本身的 long 類型,而不是 java 自帶的 long
修改Pointer以下:
class Pointer {
MyLong x = new MyLong();
MyLong y = new MyLong();
}
class MyLong {
volatile long value;
long p1, p2, p3, p4, p5, p6, p7;
}
複製代碼
同時把 pointer.x++;
修改成 pointer.x.value++;
,把 pointer.y++;
修改成 pointer.y.value++;
,再次運行程序發現時間是724ms
。
(3)使用 @sun.misc.Contended 註解(java8)
修改 MyLong 以下:
@sun.misc.Contended
class MyLong {
volatile long value;
}
複製代碼
默認使用這個註解是無效的,須要在JVM啓動參數加上-XX:-RestrictContended
纔會生效,,再次運行程序發現時間是718ms
。
注意,以上三種方式中的前兩種是經過加字段的形式實現的,加的字段又沒有地方使用,可能會被jvm優化掉,因此建議使用第三種方式。
(1)CPU具備多級緩存,越接近CPU的緩存越小也越快;
(2)CPU緩存中的數據是以緩存行爲單位處理的;
(3)CPU緩存行能帶來免費加載數據的好處,因此處理數組性能很是高;
(4)CPU緩存行也帶來了弊端,多線程處理不相干的變量時會相互影響,也就是僞共享;
(5)避免僞共享的主要思路就是讓不相干的變量不要出如今同一個緩存行中;
(6)一是每兩個變量之間加七個 long 類型;
(7)二是建立本身的 long 類型,而不是用原生的;
(8)三是使用 java8 提供的註解;
java中有哪些類避免了僞共享的干擾呢?
還記得咱們前面介紹過的 ConcurrentHashMap 的源碼解析嗎?
裏面的 size() 方法使用的是分段的思想來構造的,每一個段使用的類是 CounterCell,它的類上就有 @sun.misc.Contended 註解。
不知道的能夠關注個人公衆號「彤哥讀源碼」查看歷史消息找到這篇文章看看。
除了這個類,java中還有個 LongAdder 也使用了這個註解避免僞共享,下一章咱們將一塊兒學習 LongAdder 的源碼分析,敬請期待。
你還知道哪些避免僞共享的應用呢?
歡迎關注個人公衆號「彤哥讀源碼」,查看更多源碼系列文章, 與彤哥一塊兒暢遊源碼的海洋。