關於偽共享的文章已經很多了,對於多線程編程來說,特別是多線程處理列表和數組的時候,要非常注意偽共享的問題。否則不僅無法發揮多線程的優勢,還可能比單線程性能還差。隨著JAVA版本的更新,再各個版本上減少偽共享的做法都有區別,一不小心代碼可能就失效了,要注意進行測試。這篇文章總結一下。
關於偽共享講解最清楚的是這篇文章《剖析Disruptor:為什麼會這麼快?(三)偽共享》,我這裡就直接摘抄其對偽共享的解釋:
緩存系統中是以緩存行(cache line)為單位存儲的。緩存行是2的整數冪個連續字節,一般為32-256個字節。最常見的緩存行大小是64個字節。當多線程修改互相獨立的變量時,如 果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享。緩存行上的寫競爭是運行在SMP系統中並行線程實現可伸縮性最重要的限制因素。有 人將偽共享描述成無聲的性能殺手,因為從代碼中很難看清楚是否會出現偽共享。
為了讓可伸縮性與線程數呈線性關系,就必須確保不會有兩個線程往同一個變量或緩存行中寫。兩個線程寫同一個變量可以在代碼中發現。為了確定互相獨立的變量 是否共享了同一個緩存行,就需要了解內存布局,或找個工具告訴我們。Intel VTune就是這樣一個分析工具。本文中我將解釋Java對象的內存布局以及我們該如何填充緩存行以避免偽共享。
圖1說明了偽共享的問題。在核心1上運行的線程想更新變量X,同時核心2上的線程想要更新變量Y。不幸的是,這兩個變量在同一個緩存行中。每個線程都要去 競爭緩存行的所有權來更新變量。如果核心1獲得了所有權,緩存子系統將會使核心2中對應的緩存行失效。當核心2獲得了所有權然後執行更新操作,核心1就要 使自己對應的緩存行失效。這會來來回回的經過L3緩存,大大影響了性能。如果互相競爭的核心位於不同的插槽,就要額外橫跨插槽連接,問題可能更加嚴重。
解決偽共享的辦法是使用緩存行填充,使一個對象占用的內存大小剛好為64bytes或它的整數倍,這樣就保證了一個緩存行裡不會有多個對象。《剖析Disruptor:為什麼會這麼快?(三)偽共享》提供了緩存行填充的例子:
public final class FalseSharing
implements Runnable
{
public final static int NUM_THREADS = 4; // change
public final static long ITERATIONS = 500L * 1000L * 1000L;
private final int arrayIndex;
private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
static
{
for (int i = 0; i < longs.length; i++)
{
longs[i] = new VolatileLong();
}
}
public FalseSharing(final int arrayIndex)
{
this.arrayIndex = arrayIndex;
}
public static void main(final String[] args) throws Exception
{
final long start = System.nanoTime();
runTest();
System.out.println("duration = " + (System.nanoTime() - start));
}
private static void runTest() throws InterruptedException
{
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < threads.length; i++)
{
threads[i] = new Thread(new FalseSharing(i));
}
for (Thread t : threads)
{
t.start();
}
for (Thread t : threads)
{
t.join();
}
}
public void run()
{
long i = ITERATIONS + 1;
while (0 != --i)
{
longs[arrayIndex].value = i;
}
}
public final static class VolatileLong
{
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // comment out
}
}
VolatileLong通過填充一些無用的字段p1,p2,p3,p4,p5,p6,再考慮到對象頭也占用8bit, 剛好把對象占用的內存擴展到剛好占64bytes(或者64bytes的整數倍)。這樣就避免了一個緩存行中加載多個對象。但這個方法現在只能適應JAVA6 及以前的版本了。
(注:如果我們的填充使對象size大於64bytes,比如多填充16bit – public long p1, p2, p3, p4, p5, p6, p7, p8;。理論上同樣應該避免偽共享問題,但事實是這樣的話執行速度同樣慢幾倍,只比沒有使用填充好一些而已。還沒有理解其原因。所以測試下來,必須是64bytes的整數倍)
上面這個例子在JAVA 7下已經不適用了。因為JAVA 7會優化掉無用的字段,可以參考《False Sharing && Java 7》。
因此,JAVA 7下做緩存行填充更麻煩了,需要使用繼承的辦法來避免填充被優化掉,《False Sharing && Java 7》裡的例子我覺得不是很好,於是我自己做了一些優化,使其更通用:
public final class FalseSharing implements Runnable {
public static int NUM_THREADS = 4; // change
public final static long ITERATIONS = 500L * 1000L * 1000L;
private final int arrayIndex;
private static VolatileLong[] longs;
public FalseSharing(final int arrayIndex) {
this.arrayIndex = arrayIndex;
}
public static void main(final String[] args) throws Exception {
Thread.sleep(10000);
System.out.println("starting....");
if (args.length == 1) {
NUM_THREADS = Integer.parseInt(args[0]);
}
longs = new VolatileLong[NUM_THREADS];
for (int i = 0; i < longs.length; i++) {
longs[i] = new VolatileLong();
}
final long start = System.nanoTime();
runTest();
System.out.println("duration = " + (System.nanoTime() - start));
}
private static void runTest() throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new FalseSharing(i));
}
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
}
public void run() {
long i = ITERATIONS + 1;
while (0 != --i) {
longs[arrayIndex].value = i;
}
}
}
public class VolatileLongPadding {
public volatile long p1, p2, p3, p4, p5, p6; // 注釋
}
public class VolatileLong extends VolatileLongPadding {
public volatile long value = 0L;
}
把padding放在基類裡面,可以避免優化。(這好像沒有什麼道理好講的,JAVA7的內存優化算法問題,能繞則繞)。不過,這種辦法怎麼看都有點煩,借用另外一個博主的話:做個java程序員真難。
在JAVA 8中,緩存行填充終於被JAVA原生支持了。JAVA 8中添加了一個@Contended的注解,添加這個的注解,將會在自動進行緩存行填充。以上的例子可以改為:
public final class FalseSharing implements Runnable {
public static int NUM_THREADS = 4; // change
public final static long ITERATIONS = 500L * 1000L * 1000L;
private final int arrayIndex;
private static VolatileLong[] longs;
public FalseSharing(final int arrayIndex) {
this.arrayIndex = arrayIndex;
}
public static void main(final String[] args) throws Exception {
Thread.sleep(10000);
System.out.println("starting....");
if (args.length == 1) {
NUM_THREADS = Integer.parseInt(args[0]);
}
longs = new VolatileLong[NUM_THREADS];
for (int i = 0; i < longs.length; i++) {
longs[i] = new VolatileLong();
}
final long start = System.nanoTime();
runTest();
System.out.println("duration = " + (System.nanoTime() - start));
}
private static void runTest() throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new FalseSharing(i));
}
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
}
public void run() {
long i = ITERATIONS + 1;
while (0 != --i) {
longs[arrayIndex].value = i;
}
}
}
import sun.misc.Contended;
@Contended
public class VolatileLong {
public volatile long value = 0L;
}
執行時,必須加上虛擬機參數-XX:-RestrictContended,@Contended注釋才會生效。很多文章把這個漏掉了,那樣的話實際上就沒有起作用。
@Contended注釋還可以添加在字段上,今後再寫文章詳細介紹它的用法。
(後記:以上代碼基於32位JDK測試,64位JDK下,對象頭大小不同,有空再測試一下)
參考
http://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html
http://mechanical-sympathy.blogspot.hk/2011/08/false-sharing-java-7.html
http://robsjava.blogspot.com/2014/03/what-is-false-sharing.html