ConccurrenthashMap 1.7 1.8を読みます.

9760 ワード

概要:
Conccurrenthashmapはjava併発において重要なクラスであり、HashTableの代わりに、同時進行可能なhashtalbeを実現する.私は1.7と1.8の中の実現をもって、それぞれConcerenthashMapのキーデータ構造とキー関数を説明します.彼がどのように合併を実現したのかを見てみます.
JDK 1.7中の実現
JDK 1.7のConcerenthashMapはセグメントロックの設計を採用しています.まずデータ構造を見てみます.ConcerenthashMapにはsegment配列が含まれています.各segmentにはもう一つのHash Entry配列が含まれています.つまり、各segmentには完全なhashtableが含まれています.上記のsegment構造の定義を見てみましょう.
    static final class Segment extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
        transient volatile HashEntry[] table;
        transient int count;
        transient int modCount;
        transient int threshold;
        final float loadFactor;
        ... ...
}
方法を除いて、いくつかの熟知しているドメインを見ることができます.Hash Entry(ハッシュ配列)、threshhold(拡張閾値)、loadFactor(負荷因子)はsegmentが完全なhashmapであることを表しています.次に、ConcerenthashMapの構造関数を見てみます.
   public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel)
3つのパラメータはそれぞれ表しています.初期容量:初期容量はすべてのsegment配列の中に、いくつのhashentryが含まれていますか?initial Capacityが2のべき乗でないと、initial Capacityより大きい2のべき乗を取ります.負荷因子:デフォルト0.75.併発レベル:同時に複数のスレッドを併発することができます.concurrencyLevelはいくらですか?segmentはいくつありますか?もちろんこの値に等しい2以上のものを取ります.
次に、ConcerenthashMapのいくつかのキー関数を見てみます.put、get、rehash、size方法は彼がどのように合併を実現しているかを見てください.
put方法の実現
putメソッドの呼び出しを追跡します.
    public V put(K key, V value) {
        Segment s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }
ここで注意するのは何点ですか?1.keyとvalueは全部空ではないです.valueは判断があります.key値はhashcodeを計算する時も判断があります.2.まず合併度からkey値のsegment番号を算出し、例えば合併度が16であれば、sh値が高い4桁はsegment番号として使用されます.3.対応するsegmentを見つけ、segmentのputメソッドを呼び出します.
次にsegmentのput方法を見てみます.
        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry first = entryAt(tab, index);
                for (HashEntry e = first;;) {
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
流れは大体このようです.1.まずこのsegmentをロックします.2.hashentry配列の中で、対応する位置(索引)を見つけます.unsafeの方法を使って、配列要素の原子性を確保します.3.対応するkeyが見つかったら、この値を修正します.valueはvolatileなので、unsafeを使う方法はありません.
見ることができます.putの方法はロックをかける必要があります.この時はセグメントロックをかけます.
ゲット方法
    public V get(Object key) {
        Segment s; // manually integrate access methods to reduce overhead
        HashEntry[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }
get法はunsafeの方法を採用していて、スレッドの安全を保証しています.ここにはロックがありません.いくつかのバージョンのget方法を見ましたが、いずれもロックがないので、スレッドの安全を実現するために様々な方法を使っています.
rehash方法:
segmentのcount(segment配列で使用されている個数を指す)がthresh Holdより大きい場合は、rehash法を使用して拡大する必要があります.拡張操作もputのロック中にあり、スレッドの安全を保証します.
sizeメソッド:
    public int size() {
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        final Segment[] segments = this.segments;
        int size;
        boolean overflow; // true if size overflows 32 bits
        long sum;         // sum of modCounts
        long last = 0L;   // previous sum
        int retries = -1; // first iteration isn't retry
        try {
            for (;;) {
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment seg = segmentAt(segments, j);
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                if (sum == last)
                    break;
                last = sum;
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return overflow ? Integer.MAX_VALUE : size;
    }
size()メソッドは、2回連続して任意のmodcountが等しい場合、3回全体の数を取得することを試みる.同じでないなら、ロックをかけて数を算出します.
JDK 1.8の中の実現
JDK 1.8の中で、大きな変化がありました.段鎖を使っていません.Node配列+チェーン+赤と黒の木に変えました.
重要な変数があります.sizectl
  • 負数は、初期化または拡張が行われていることを示し、−1は初期化中であり、−NはN−1スレッドが拡張されていることを示している.
  • 正数0は、まだ初期化されていないことを示している.他の正数は次の拡大サイズを表します.
  • まずコアデータ構造を見てみます.
    static class Node implements Map.Entry {
        final int hash;
        final K key;
        volatile V val;
        volatile Node next;
    }
    
    もう2つのデータ構造TreeNode、TreeBinは、チェーンの大きさが閾値を超えたときに、チェーンを赤と黒に変えます.
    ForwardingNode、拡張機能を使用したチェーン表、拡張機能を使用して、このnodeを経験したら、他にもこのnodeを拡大しています.スキップすればいいです
    CAS操作
    このバージョンはCAS操作が多く使われています.CASとは、メモリに対応するエリアの値を比較して、期待値と同じではないかということです.等しいなら、新しい値を設定して入れます.一般的にこのように使用されて、まず対象のドメインの値を取得し、この値を期待値としてCASアルゴリズムを起動する.
    コンカレントHashMapには三つのコアのCAS操作があります.tabAt:配列中位置i上のノードcas TabAtを取得する:配列位置i上のノードsetTabAtを設定する:volatileを利用して位置i上のノードを設定する.
    いくつかのポイントを紹介します.
    initTable
        private final Node[] initTable() {
            Node[] tab; int sc;
            while ((tab = table) == null || tab.length == 0) {
                if ((sc = sizeCtl) < 0)
                    Thread.yield(); // lost initialization race; just spin
                else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if ((tab = table) == null || tab.length == 0) {
                            int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                            @SuppressWarnings("unchecked")
                            Node[] nt = (Node[])new Node,?>[n];
                            table = tab = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                    break;
                }
            }
            return tab;
        }
    
    putメソッドの呼び出し時に、戻ってテーブルがnullかどうかを判断し、nullであればinitTableを呼び出して初期化します.initTableを呼び出してsizectlの値を判断します.初期化していると、yield()を呼び出して待ちます.0なら、これは先にCASアルゴリズムを呼び出して-1に設定してから初期化します.初期化はシングルスレッド操作です.
    拡充方法transfer
    ConccurrenthashMapでの配列使用数が閾値より大きい場合は、拡張が必要です.ここの拡大容量は2つの部分に分けられています.1.シングルスレッド処理で、newTableを新規作成します.容量が元の2倍になります.2.マルチスレッド処理は、元のテーブルのデータをnewTableにコピーする.
    マルチスレッド処理のロジック:
  • は、tabAtを利用してiの位置を取得する.
  • この位置が空の場合、ロックをかけずにforwardノードに設定します.
  • 普通のノードであれば、ロックをかけて分析し、newTableのiとi+nの位置に置いて、処理が終わったら、このノードをforwardに設定します.
  • putメソッド
    ロジックが複雑ですので、コードを付けません.流れを教えてください.1.テーブル==nullの場合は、上記の初期化を呼び出します.2.テーブルの中のiの位置が空いているなら、ロックをかけずに新しいnodeを作ってcasの方式で入れます.3.当該Nodeが拡大している場合は、拡張機能を助け、helpTransfer関数を呼び出す必要があります.4.正常に添加するとロックが必要です.synchronizedを使って、衝突があれば、判断が必要です.チェーンの長さが8より大きいとき、チェーンを赤と黒の木に変えます.
    ゲット方法
    get方法はロックをかけない.CAS操作により、ロックなしのアクセスが可能です.mapが拡大している時に、forwardnodeもfind()の方法を提供して、拡大していることを保証します.対応するvalueも見つけられます.
    size()操作も大体の値です.
    concurrenthashMapは弱一致で、get、sizeは正しい値を得ることが保証できません.