1. 程式人生 > >Java8中@Contended和偽共享

Java8中@Contended和偽共享

Java8引入了@Contented這個新的註解來減少偽共享(False Sharing)的發生。本文介紹了@Contented註解並解釋了為什麼False Sharing是如何影響效能的。

快取行

CPU讀取記憶體資料時並非一次只讀一個位元組,而是會讀一段64位元組長度的連續的記憶體塊(chunks of memory),這些塊我們稱之為快取行(Cache line)。

假設你有兩個執行緒(Thread1和Thread2)都會修改同一個volatile變數x:

1 volatile long x;

如果Thread1先改變x的值,然後Thread2又去讀它:

12 Thread 1
: x=3; Thread 2: System.out.print(x);

那麼x所在快取行上的所有64個位元組的值都要被重新載入,因為CPU核心間交換資料是以快取行為最小單位的。當然Thread1和Thread2是有可能在同一個核心上執行的,但我們此處假設兩個執行緒在不同的核心上執行。

已知long型別佔8個位元組,快取行長度為64個位元組,那麼一個快取行可以儲存8個long型變數,我們已經有了一個long型的x,假設x所在快取行裡還有其他7個long型變數,v1到v7:

1 x, v1, v2, v3, v4, v5 ,v6 ,v7

偽共享(False Sharing)

這個快取行可以被許多執行緒訪問。如果其中一個修改了v2,那麼會導致Thread1和Thread2都會重新載入整個快取行。你可能會疑惑為什麼修改了v2會導致Thread1和Thread2重新載入該快取行,畢竟只是修改了v2的值啊。雖然說這些修改邏輯上是互相獨立的,但同一快取行上的資料是統一維護的,一致性的粒度並非體現在單個元素上。這種不必要的資料共享就稱之為“偽共享”(False Sharing)。

填充(Padding)

一個CPU核心在載入一個快取行時要執行上百條指令。如果一個核心要等待另外一個核心來重新載入快取行,那麼他就必須等在那裡,稱之為stall(停止運轉)。減少偽共享也就意味著減少了stall的發生,其中一個手段就是通過填充(Padding)資料的形式,來保證本應有可能位於同一個快取行的兩個變數,在被多執行緒訪問時必定位於不同的快取行。

在下面這個例子裡,我們試圖通過填充的方式,使得xv1位於不同的快取行:

1234567891011 public class FalseSharingWithPadding { public volatile long x; public
volatile long p2; // padding public volatile long p3; // padding public volatile long p4; // padding public volatile long p5; // padding public volatile long p6; // padding public volatile long p7; // padding public volatile long p8; // padding public volatile long v1;}

在你考慮使用填充之前,必須要了解的一點是JVM可能會清除無用欄位或重排無用欄位的位置,這樣的話,可能無形中又會引入偽共享。我們也沒有辦法指定物件在堆內駐留的位置。

為了避免無用欄位被消除,通常我們會用volatile修飾一下。個人建議只需為處於激烈競爭狀態的類進行填充處理,而且一般只有通過對其效能分析才能發現效能上的不同。通常在效能分析時,最好在對其迭代訪問10000次之後再去取樣,這樣可以消除JVM本身的執行時優化策略帶來的影響。

Java8和@Contended

除了對欄位進行填充之外,還有一個比較清爽的方法,那就是對需要避免陷入偽共享的欄位進行註解,這個註解暗示JVM應當將欄位放入不同的快取行,這也正是JEP142的相關內容。

該JEP引入了@Contented註解。被這個註解修飾的欄位應當和其他的欄位駐留在不同的位置。

12345 public class Point { int x; @Contended int y;}

上面的程式碼將x和y置於不同的快取行。@Contented註解將y移動到遠離物件頭部的地方,(以避免和x一起被載入到同一個快取行)。

參考