HashTableおよびConcurrentHashMapインタビューの質問の詳細な説明

ハッシュ表

機能:基礎となるデータ構造:配列+リンクリスト(内部クラスエントリの配列)

スレッドセーフ:同期されたキーワード変更(ロックの粒度が大きい、JVMロック)、効率が低い

初期化:初期化サイズは11で、負荷係数は0.75です

ストレージと拡張:キーと値がNullとして保存されると、NullPointerExceptionがスローされます

容量拡張:実際のサイズが容量拡張しきい値=初期化サイズ*負荷係数より大きい場合、2倍プラス1
 

ソースコード分析と組み合わせる:

データ構造:配列+リンクリスト

/**
 * The hash table data.
 */
private transient Entry<?,?>[] table;
...
private static class Entry<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Entry<K,V> next;
    ...

 

スレッドセーフ:同期に依存

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) { // VALUE值不为NULL
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode(); // key值为Null会导致空指针
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}

例としてputメソッドを取り上げ、putメソッドはKEY / VALUE値もNULLとして処理します

ハッシュ値演算:KEYのHashCodeを直接使用し、次に最大のint値と演算を使用して、配列の長さの係数(デフォルトは11)を取得します。

private void addEntry(int hash, K key, V value, int index) {
    modCount++;

    Entry<?,?> tab[] = table;
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode(); // 直接使用KEY的HashCode
        index = (hash & 0x7FFFFFFF) % tab.length; //最大int值与运算再对数组长度(默认11)取模
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

要約:

インタビューに対処する方法を学ぶ必要があります。ただし、実際の本番環境ではHashTableはあまり使用されません。スレッドの安全性にはHashMapは必要ありません(1.8では関連する最適化も実行されます)。スレッドの安全性にはConcurrentHashMapが必要です。これはより効率的で、JDK1.8も最適化されています。

 

ConcuurentHashMap

 

特徴:

データ構造:JDK1.7配列+リンクリスト内部クラス(エントリ配列)+セグメントロック(スレッドの安全性を確保するため)内部クラス:エントリ、セグメント(セグメントロック)

JDK1.8配列+リンクリスト+レッドブラックツリー+(CASメカニズム+同期[スレッドセーフを保証])内部クラス:HashEntry

その他の点では、ConcurrentHashMapはHashMapと非常に似ています。

 

ソースコード分析

JDK1.7

/**
 * Segments are specialized versions of hash tables.  This
 * subclasses from ReentrantLock opportunistically, just to
 * simplify some locking and avoid separate construction.
 */
static final class Segment<K,V> extends ReentrantLock 
                                        implements Serializable {
                                        ...
transient volatile HashEntry<K,V>[] table;
...           

 

注:ReentrantLockは、各セグメントのスレッド同期(原子性)を実装します。同時に、揮発性HashEntryはHashEntryの可視性と順序を保証します。

JDK1.8

// Unsafe mechanics CAS机制相关的Unsafe类
private static final sun.misc.Unsafe U;

/**
 * Initializes table, using the size recorded in sizeCtl.
 */
private final Node<K,V>[] initTable() {
    Node<K,V>[] 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)) {
            // CAS机制
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

 

1:Unsafeクラス(CASメカニズム)を使用して、データ変更の原子性を実現します; 2:Get、putなどのメソッドは、Synchronizedキーワードを使用して郡のセキュリティを実現します。

ここで、Synchronizedキーワードが使用され、CASメカニズムがスレッドセーフを実現するためにも使用される理由を理解してください。

私の理解では、Synchronizedはストレージと取得のメソッドを変更します。CASメカニズムはコレクションサイズの変更を目的としています。JDK1.8では、揮発性変数baseCountを使用して要素の数を記録しています。新しいデータが挿入されるか、データが削除されると、addCount()メソッドを通じてbaseCountを更新します

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    // U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
    // 该方法判断本地存储的baseCount是否与缓存中一致,若一直才容许修改
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

 

要約:

JDK1.7からJDK1.8まで、ConcurrentHashMapのロックの細分性が改善されました。元のセグメント化されたロック(複数のセグメント、同じセグメントで同期)で構成され、配列テーブルに基づいてロックされます。最大パフォーマンスの向上。

注:ReentrantLockについては、後続のスレッドセーフティで詳しく説明します。AQSは後続のブログでも更新されます。

安全でないクラスとそれに関連するCASメカニズムについても、スレッドセーフティで詳しく説明します。

元の記事を27件公開 賞賛された0 9932を訪問

おすすめ

転載: blog.csdn.net/weixin_38246518/article/details/105542976