併發程式設計 —— ConcurrentHashMap size 方法原理分析

莫那·魯道發表於2019-03-04

前言

ConcurrentHashMap 博大精深,從他的 50 多個內部類就能看出來,似乎 JDK 的併發精髓都在裡面了。但他依然擁有體驗良好的 API 給我們使用,程式設計師根本感覺不到他內部的複雜。但,他內部的每一個方法都複雜無比,就連 size 方法,都挺複雜的。

今天就一起來看看這個 size 方法。

size 方法

程式碼如下:

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}
複製程式碼

最大返回 int 最大值,但是這個 Map 的長度是有可能超過 int 最大值的,所以 JDK 8 增了 mappingCount 方法。程式碼如下:

public long mappingCount() {
    long n = sumCount();
    return (n < 0L) ? 0L : n; // ignore transient negative values
}
複製程式碼

相比較 size 方法,mappingCount 方法的返回值是 long 型別。所以不必限制最大值必須是 Integer.MAX_VALUE。而 JDK 推薦使用這個方法。但這個返回值依然不一定絕對準確。

從這兩個方法中可以看出,sumCount 方法是核心。

sumCount 方法實現

程式碼如下:

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}
複製程式碼

上面的方法邏輯:當 counterCells 不是 null,就遍歷元素,並和 baseCount 累加。

兩個屬性 : baseCount 和 counterCells。

先看 baseCount。

    /**
     * Base counter value, used mainly when there is no contention,
     * but also as a fallback during table initialization
     * races. Updated via CAS.
     * 當沒有爭用時,使用這個變數計數。
     */
    private transient volatile long baseCount;
複製程式碼

一個 volatile 的變數,在 addCount 方法中會使用它,而 addCount 方法在 put 結束後會呼叫。在 addCount 方法中,會對這個變數做 CAS 加法。

image.png

但是如果併發導致 CAS 失敗了,怎麼辦呢?使用 counterCells。

image.png

如果上面 CAS 失敗了,在 fullAddCount 方法中,會繼續死迴圈操作,直到成功。

image.png

而這個 CounterCell 類又是上面鬼呢?

// 一種用於分配計數的填充單元。改編自LongAdder和Striped64。請檢視他們的內部文件進行解釋。
@sun.misc.Contended 
static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}
複製程式碼

使用了 @sun.misc.Contended 標記的類,內部一個 volatile 變數。註釋說,改編自LongAdder和Striped64,關於這兩個類,請看 Java8 Striped64 和 LongAdder

而關於這個註解,有必要解釋一下。這個註解標識著這個類防止需要防止 "偽共享".

說說偽共享。引用 一下別人的說法:

避免偽共享(false sharing)。 先引用個偽共享的解釋: 快取系統中是以快取行(cache line)為單位儲存的。快取行是2的整數冪個連續位元組, 一般為32-256個位元組。最常見的快取行大小是64個位元組。當多執行緒修改互相獨立的變數時, 如果這些變數共享同一個快取行,就會無意中影響彼此的效能,這就是偽共享。

所以偽共享對效能危害極大。

JDK 8 版本之前沒有這個註解,Doug Lea 使用拼接來解決這個問題,把快取行加滿,讓快取之間的修改互不影響。

在我的機器上測試,加和不加這個註解的效能差距達到了 5 倍。

總結

好了,關於 Size 方法就簡單介紹到這裡。總結一下:

JDK 8 推薦使用mappingCount 方法,因為這個方法的返回值是 long 型別,不會因為 size 方法是 int 型別限制最大值(size 方法是介面定義的,不能修改)。

在沒有併發的情況下,使用一個 baseCount volatile 變數就足夠了,當併發的時候,CAS 修改 baseCount 失敗後,就會使用 CounterCell 類了,會建立一個這個物件,通常物件的 volatile value 屬性是 1。在計算 size 的時候,會將 baseCount 和 CounterCell 陣列中的元素的 value 累加,得到總的大小,但這個數字仍舊可能是不準確的。

還有一個需要注意的地方就是,這個 CounterCell 類使用了 @sun.misc.Contended 註解標識,這個註解是防止偽共享的。是 1.8 新增的。使用時,需要加上 -XX:-RestrictContended 引數。

相關文章