僞共享和緩存行填充,從Java 6, Java 7 到Java 8

關於僞共享的文章已經不少了,對於多線程編程來講,特別是多線程處理列表和數組的時候,要很是注意僞共享的問題。不然不只沒法發揮多線程的優點,還可能比單線程性能還差。隨着JAVA版本的更新,再各個版本上減小僞共享的作法都有區別,一不當心代碼可能就失效了,要注意進行測試。這篇文章總結一下。html

 

什麼是僞共享

關於僞共享講解最清楚的是這篇文章《剖析Disruptor:爲何會這麼快?(三)僞共享》,我這裏就直接摘抄其對僞共享的解釋:java

 

緩存系統中是以緩存行(cache line)爲單位存儲的。緩存行是2的整數冪個連續字節,通常爲32-256個字節。最多見的緩存行大小是64個字節。當多線程修改互相獨立的變量時,如 果這些變量共享同一個緩存行,就會無心中影響彼此的性能,這就是僞共享。緩存行上的寫競爭是運行在SMP系統中並行線程實現可伸縮性最重要的限制因素。有 人將僞共享描述成無聲的性能殺手,由於從代碼中很難看清楚是否會出現僞共享。程序員

爲了讓可伸縮性與線程數呈線性關係,就必須確保不會有兩個線程往同一個變量或緩存行中寫。兩個線程寫同一個變量能夠在代碼中發現。爲了肯定互相獨立的變量 是否共享了同一個緩存行,就須要瞭解內存佈局,或找個工具告訴咱們。Intel VTune就是這樣一個分析工具。本文中我將解釋Java對象的內存佈局以及咱們該如何填充緩存行以免僞共享。算法

cache-line.png

圖1說明了僞共享的問題。在覈心1上運行的線程想更新變量X,同時核心2上的線程想要更新變量Y。不幸的是,這兩個變量在同一個緩存行中。每一個線程都要去 競爭緩存行的全部權來更新變量。若是核心1得到了全部權,緩存子系統將會使核心2中對應的緩存行失效。當核心2得到了全部權而後執行更新操做,核心1就要 使本身對應的緩存行失效。這會來來回回的通過L3緩存,大大影響了性能。若是互相競爭的核心位於不一樣的插槽,就要額外橫跨插槽鏈接,問題可能更加嚴重。編程

 

JAVA 6下的方案

解決僞共享的辦法是使用緩存行填充,使一個對象佔用的內存大小恰好爲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,好比多填充16bytes– public long p1, p2, p3, p4, p5, p6, p7, p8;。理論上一樣應該避免僞共享問題,但事實是這樣的話執行速度一樣慢幾倍,只比沒有使用填充好一些而已。尚未理解其緣由。因此測試下來,必須是64bytes的整數倍)多線程

 

JAVA 7下的方案

上面這個例子在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 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

相關文章
相關標籤/搜索