1. 程式人生 > >Disruptor以及@Contended註解

Disruptor以及@Contended註解

CAS的佇列都是無界

這個與CAS的原理有關係,它是一個不斷嘗試的機制,直到返回最終的正確結果或者完成操作,其原始碼基本上都是這樣的實現:

public final int getAndAdd(int delta) {
    for (;;) {
        int current = get();
        int next = current + delta;
        if (compareAndSet(current, next))
            return current;
    }
}

如果有界應該會加鎖處理邊界問題。

快取行實驗

實驗程式碼在原文中有,這裡不再說明,只貼一下實驗結果:

Loop times:10ms
Loop times:46ms

這裡機器的配置3.2GHz,32G memory,機器配置的提升是實驗資料提升的主要原因,其他實驗的相關資料基本與原文中相似,這裡不再贅述。

java 8 Contended註解

在Java 8中,可以採用@Contended在類級別上的註釋,來進行快取行填充。這樣,可以解決多執行緒情況下的偽共享衝突問題。

Contended可以用於類級別的修飾,同時也可以用於欄位級別的修飾,當應用於欄位級別時,被註釋的欄位將和其他欄位隔離開來,會被載入在獨立的快取行上。在欄位級別上,@Contended還支援一個“contention group”屬性(Class-Level不支援),同一group的欄位們在記憶體上將是連續(64位元組範圍內),但和其他他欄位隔離開來。

@Contended註釋的行為如下所示:

A,在類上應用Contended:

@Contended
    public static class ContendedTest2 {
        private Object plainField1;
        private Object plainField2;
        private Object plainField3;
        private Object plainField4;
    }

將使整個欄位塊的兩端都被填充:(以下是使用 –XX:+PrintFieldLayout的輸出)(翻譯註:注意前面的@140表示欄位在類中的地址偏移)

TestContended$ContendedTest2: field layout
    Entire class is marked contended
     @140 --- instance fields start ---
     @140 "plainField1" Ljava.lang.Object;
     @144 "plainField2" Ljava.lang.Object;
     @148 "plainField3" Ljava.lang.Object;
     @152 "plainField4" Ljava.lang.Object;
     @288 --- instance fields end ---
     @288 --- instance ends ---

注意,我們使用了128 bytes的填充 – 2倍於大多數硬體快取行的大小(cache line一般為64 bytes) – 來避免相鄰扇區預取導致的偽共享衝突。

B,在欄位上應用Contended:

public static class ContendedTest1 {
        @Contended
        private Object contendedField1;
        private Object plainField1;
        private Object plainField2;
        private Object plainField3;
        private Object plainField4;
    }

將導致該欄位從連續的欄位塊中分離開來並高效的新增填充:

TestContended$ContendedTest1: field layout
     @ 12 --- instance fields start ---
     @ 12 "plainField1" Ljava.lang.Object;
     @ 16 "plainField2" Ljava.lang.Object;
     @ 20 "plainField3" Ljava.lang.Object;
     @ 24 "plainField4" Ljava.lang.Object;
     @156 "contendedField1" Ljava.lang.Object; (contended, group = 0)
     @288 --- instance fields end ---
     @288 --- instance ends ---

C, 註解多個欄位使他們分別被填充:

public static class ContendedTest4 {
        @Contended
        private Object contendedField1;

        @Contended
        private Object contendedField2;

        private Object plainField3;
        private Object plainField4;
    }

被註解的2個欄位都被獨立地填充:

TestContended$ContendedTest4: field layout
     @ 12 --- instance fields start ---
     @ 12 "plainField3" Ljava.lang.Object;
     @ 16 "plainField4" Ljava.lang.Object;
     @148 "contendedField1" Ljava.lang.Object; (contended, group = 0)
     @280 "contendedField2" Ljava.lang.Object; (contended, group = 0)
     @416 --- instance fields end ---
     @416 --- instance ends ---

在有些cases中,你會想對欄位進行分組,同一組的欄位會和其他欄位有訪問衝突,但是和同一組的沒有。例如,(同一個執行緒的)程式碼同時更新2個欄位是很常見的情況。如果同時把2個欄位都新增@Contended註解是足夠的(翻譯註:但是太足夠了),但我們可以通過去掉他們之間的填充,來優化它們的記憶體空間佔用。為了區分組,我們有一個引數“contention group”來描述:

public static class ContendedTest5 {
        @Contended("updater1")
        private Object contendedField1;

        @Contended("updater1")
        private Object contendedField2;

        @Contended("updater2")
        private Object contendedField3;

        private Object plainField5;
        private Object plainField6;
    }

記憶體佈局是:

TestContended$ContendedTest5: field layout
     @ 12 --- instance fields start ---
     @ 12 "plainField5" Ljava.lang.Object;
     @ 16 "plainField6" Ljava.lang.Object;
     @148 "contendedField1" Ljava.lang.Object; (contended, group = 12)
     @152 "contendedField2" Ljava.lang.Object; (contended, group = 12)
     @284 "contendedField3" Ljava.lang.Object; (contended, group = 15)
     @416 --- instance fields end ---
     @416 --- instance ends ---

注意contendedField1contendedField1 和contendedField2和其他欄位之間有填充,但是它們之間是緊挨著的,類內偏移量為4 bytes,為一個物件的大小。

下面我們來做一個測試,看@Contended在欄位級別,並且帶分組的情況下,是否能解決偽快取問題。

import sun.misc.Contended;

public class VolatileLong {
    @Contended("group0")
    public volatile long value1 = 0L;  
    @Contended("group0")
    public volatile long value2 = 0L;  
    
    @Contended("group1")
    public volatile long value3 = 0L;  
    @Contended("group1")
    public volatile long value4 = 0L;  
}

我們用2個執行緒來修改欄位

測試1:執行緒0修改value1和value2;執行緒1修改value3和value4;他們都在同一組中。

測試2:執行緒0修改value1和value3;執行緒1修改value2和value4;他們在不同組中。

測試1
public final class FalseSharing implements Runnable {
    public final static long ITERATIONS = 500L * 1000L * 1000L;
    private static Volatile Long volatileLong;
    private String groupId;

    public FalseSharing(String groupId) {
        this.groupId = groupId;
    }

    public static void main(final String[] args) throws Exception {
        // Thread.sleep(10000);
        System.out.println("starting....");

        volatileLong = new VolatileLong();
        final long start = System.nanoTime();
        runTest();
        System.out.println("duration = " + (System.nanoTime() - start));
    }

    private static void runTest() throws InterruptedException {
        Thread t0 = new Thread(new FalseSharing("t0"));
        Thread t1 = new Thread(new FalseSharing("t1"));
        t0.start();
        t1.start();
        t0.join();
        t1.join();
    }

    public void run() {
        long i = ITERATIONS + 1;
        if (groupId.equals("t0")) {
            while (0 != --i) {
                volatileLong.value1 = i;
                volatileLong.value2 = i;
            }
        } else if (groupId.equals("t1")) {
            while (0 != --i) {
                volatileLong.value3 = i;
                volatileLong.value4 = i;
            }
        }
    }
}
測試2:(基於以上程式碼修改下面的部分)
public void run() {
        long i = ITERATIONS + 1;
        if (groupId.equals("t0")) {
            while (0 != --i) {
                volatileLong.value1 = i;
                volatileLong.value3 = i;
            }
        } else if (groupId.equals("t1")) {
            while (0 != --i) {
                volatileLong.value2 = i;
                volatileLong.value4 = i;
            }
        }
    }

原作者的測試資料:

測試1:

starting....
duration = 16821484056

測試2:

starting....
duration = 39191867777

下面是我的測試資料:

測試1:

starting....
duration = 26198119279

測試2:

starting....
duration = 24851524100

我的結果不知道為什麼沒有提升一倍,困惑!(可以看出,如果同一執行緒修改的是同一“contention group”中的欄位,沒有偽共享衝突,比有偽共享衝突的情況要快1倍多。)

後記:

測試3:不使用@Contended

public class VolatileLong {
    public volatile long value1 = 0L;  
    public volatile long value2 = 0L;  
    public volatile long value3 = 0L;  
    public volatile long value4 = 0L;  
}

結果:

starting....
duration = 23347254719