HashMapのスレッドの不安定性とはどういう意味ですか?

Javaプログラマーがこれまでに尋ねられた1つの質問は次のとおりです。

  • HashMapがスレッドセーフでないのはなぜですか?
  • ConcurrentHashMapスレッドが安全なのはなぜですか?

HashMapがスレッドセーフでないのはなぜですか?

フェイルファストメカニズム

  • イテレーターの使用中に他のスレッドがマップを変更すると、ConcurrentModificationExceptionがスローされます。これは、いわゆるフェイルファスト戦略です。
  • ソースコードを見ると、ArrayList、LinkedList、およびHashMap内に、int型のmodCountオブジェクトがあることがわかります。上記のコレクションのコンテンツを変更すると、この値が増加します。modCountはイテレーターで使用されます。イテレーターのコンストラクターには、expectedModCount = modCountというコード行があります。nextEntityおよびremoveメソッドの呼び出し中に、modCount!= expectedModCountの場合、ConcurrentModificationExceptionがスローされます。
final Entry<K,V> nextEntry() {
    
    
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
public void remove() {
    
    
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

エンドレスループ

  • HashMapには2つの重要な属性があります。1つは容量で、もう1つは負荷係数です。容量は2のn乗である必要があります。これは、キーハッシュの後の値を配列内に均等に分散できるようにするためです。現在のノード数が容量*負荷率と等しくなった後、元のサイズの2倍に拡張する必要があります。このプロセスは再ハッシュと呼ばれます。
  • ハッシュマップはヘッダー挿入方法であるため、新しいノードは配列のヘッドノードに挿入されます。スレッド1がnewTable [i] = eへの再ハッシュを実行すると、スレッド1は一時停止されます。この時点で、スレッド2は再ハッシュの実行を開始して完了します。これにより、ノードの循環参照の問題が発生します。
void transfer(Entry[] newTable, boolean rehash) {
    
    
   int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
    
    // table是老数组
        while(null != e) {
    
    
            Entry<K,V> next = e.next;
            if (rehash) {
    
    
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

ConcurrentHashMapスレッドが安全なのはなぜですか?

java7の構造

ここに写真の説明を挿入

  • ConcurrentHashMapのデータ構造は、Segment配列と複数のHashEntryで構成されています。Segment配列の意味は、大きなテーブルを複数の小さなテーブルに分割してロックすることであり、各Segment要素はHashEntry配列+リンクリストを格納します。
  • データを検索するとき、最初にセグメントが検索され、次にバケットがセグメントに配置されます。
  • 複数のスレッドが同じセグメントで動作する場合、セグメントロックReentrantLockがトリガーされます。これは、セグメントロックの基本的な実現原理です。
  • セグメントはReentrantLockロックを継承し、配列HashEntry []を格納するために使用されます。初期化後にセグメントを拡張することはできませんが、HashEntryは拡張できます。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    
    
}
  • データを挿入するときは、最初にセグメントを見つけ、セグメントのputメソッドを呼び出して、新しいノードをセグメントに挿入する必要があります。セグメント内のロックを取得します。取得が失敗した場合は、scanAndLockForPutメソッドに移動します。取得が成功した場合は戻り、失敗した場合は、成功するまでループ内で取得を続けます。
public V put(K key, V value) {
    
    
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    // 根据key的hash再次进行hash运算
    int hash = hash(key.hashCode());
    // 基于hash定位segment数组的索引。
    // hash值是int值,32bits。segmentShift=28,无符号右移28位,剩下高4位,其余补0。
    // segmentMask=15,二进制低4位全部是1,所以j相当于hash右移后的低4位。
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
    // 找到对应segment
        s = ensureSegment(j);
    // 调用该segment的put方法,将新节点插入segment中
    return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    
    
    // 是否获取锁,失败自旋获取锁(直到成功)
    // 拿到结点之后,对结点进行插入操作。
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value); 
}

java8の構造

ここに写真の説明を挿入

  • Java8はセグメントを使用せず、セグメントロックを使用しなくなりましたが、大きな配列を使用します。ハッシュの衝突を解決するために、リンクリストの長さが特定の値(デフォルトは8)を超えると、リンクリストは赤黒のツリーに変換されます。アドレスの複雑さはO(n)からOlog(n)に変換されます。
  • 同時実行制御は、同期およびCASを使用して動作します。
  • ConcurrentHashMapのコンストラクターは空の関数であり、初期化はputで実装されます。
  • 並行性の可視性を確保するために、ノードの値と次の両方がvolatileで変更されます。同時実行の可視性を確保するために、valueとnextの両方がvolatileで変更されます。

ペシミスティックロック:各リクエストは最初にデータをロックし、次にデータ操作を実行し、最後にロックを解除するため、データの排他性と正確性を完全に保証できます。ロックとロックの解除のプロセスによって消費が発生するため、パフォーマンスは高くありません。 ;
オプティミスティックロック:競合が発生していないと仮定して、競合が検出された場合は失敗し、成功するまで再試行します。データベースの楽観的なロックは、バージョン制御によって実現されます。

  • 置くプロセスでは、compareAndSwapIntとcompareAndSwapObjectがテーブルと楽観的ロックの実装であるcasTabAtの初期化に使用されるため、試行を続けます。
  • 追加する特定の配列のノードタブ[i]をロックし、リンクリストまたは赤黒ツリーをトラバースして、データを挿入します
// 不参与序列化
transient volatile Node<K,V>[] table; // volatile保证线程间可见
 // 记录容器的容量大小,通过CAS更新
private transient volatile long baseCount;

/**
 * 初始化和扩容控制参数。为负数时表示table正在被初始化或resize:-1(初始化),-(1+扩容线程数)

 * sizeCtl默认值为0,大于0是扩容的阀值
 */
private transient volatile int sizeCtl;

final V putVal(K key, V value, boolean onlyIfAbsent) {
    
    
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
     // while(true)循环,不断的尝试,因为在table的初始化和casTabAt用到了compareAndSwapInt、compareAndSwapObject
        for (Node<K,V>[] tab = table;;) {
    
    
            Node<K,V> f; int n, i, fh;
       // 如果数组(HashMap)还未初始化,进行初始化操作(CAS操作)
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
       // 计算新插入数据在数组(HashMap)中的位置,如果该位置上的节点为null,直接放入数据(CAS操作)
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    
    
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
       // 如果该节点的hash值为MOVED,说明正在进行扩容操作或者已经扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
       // 
            else {
    
    
                V oldVal = null;
                synchronized (f) {
    
      // 对特定数组节点tab[i]加锁
                    if (tabAt(tab, i) == f) {
    
     // 判断tab[i]是否有变化
                        if (fh >= 0) {
    
     // 插入操作
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
    
     // 遍历链表
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
    
     // 如果新插入值和tab[i]处的hash值和key值一样,进行替换
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
    
     // 如果此节点为尾部节点,把此节点的next引用指向新数据节点
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
    
     // 如果是一颗红黑树
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
    
    
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
    
    
                    if (binCount >= TREEIFY_THRESHOLD) //如果数组节点的链表长度超过限定长度,转换为一颗红黑树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount); 
        return null;
    }

おすすめ

転載: blog.csdn.net/u010659877/article/details/108790315