マルチスレッドでのHashMap無限ループ問題の詳細な説明

みなさん、こんにちは。今日、テクニカルブログを読んでいたときに、タイトルが示すように、非常に興味深い質問を目にしました。------「マルチスレッドの場合、HashMapの無限ループに関して、私がJavaSEを初めて学んだとき、この問題に気づきました。当時は知識が足りず、深く勉強していませんでした。本日は詳しくお話しし、皆様のお役に立てれば幸いです。

テキストの始まり:

JavaのHashMapはスレッドセーフではありません。マルチスレッドでは、ConcurrentHashMapを使用する必要があります。

[HashMap]マルチスレッドでの問題(主にここで無限ループの問題について話します):

1.マルチスレッドのput操作の後、get操作により無限ループが発生します。
2.マルチスレッドのputnon-NULL要素の後、get操作はNULL値を取得します。
3.マルチスレッドのput操作により、要素が失われます。

1. Jdk7バージョンで無限ループが発生するのはなぜですか?

マルチスレッドでスレッドセーフでないHashMapを使用すると、シングルスレッドはまったく表示されません)

HashMapは、リンクリストを使用してハッシュの競合を解決します。これはリンクリスト構造であるため、ループ中にスレッドがこのHashMapに対してget操作を実行する限り、クローズドリンクを簡単に形成できます。エンドレスループが発生します。

シングルスレッドの場合、HashMapのデータ構造を操作するスレッドは1つだけであり、閉ループを生成することはできません。

これは、マルチスレッドの同時実行の場合、つまりput操作の場合にのみ発生します。
size> initialCapacity * loadFactorの場合、HashMapは再ハッシュ操作を実行し、HashMapの構造が大幅に変更されます。この時点で2つのスレッドが再ハッシュ操作をトリガーし、閉ループが発生した可能性があります。

2.それはどのように起こりましたか:

データの保存put():

	public V put(K key, V value)
	{
    
    
		......
		//算Hash值
		int hash = hash(key.hashCode());
		int i = indexFor(hash, table.length);
		//如果该key已被插入,则替换掉旧的value (链接操作)
		for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    
    
			Object k;
			if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    
    
				V oldValue = e.value;
				e.value = value;
				e.recordAccess(this);
				return oldValue;
			}
		}
		modCount++;
		//该key不存在,需要增加一个结点
		addEntry(hash, key, value, i);
		return null;
	}

HashMapに要素を配置するとき、最初にキーのハッシュ値に従って配列内のこの要素の位置(つまり添え字)を取得し、次にこの要素を対応する位置に配置できます。
この要素の場所にすでに他の要素が保存されている場合、同じ位置にある要素はリンクリストの形式で保存され、チェーンの先頭に新しく追加された要素があり、最後に以前に追加された要素がありますチェーンの。

容量が標準のaddEntryを超えているかどうかを確認します。

	void addEntry(int hash, K key, V value, int bucketIndex)
	{
    
    
		Entry<K,V> e = table[bucketIndex];
		table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
		//查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
		if (size++ >= threshold)
			resize(2 * table.length);
	}

サイズがしきい値を超えた場合は、サイズ変更操作が必要であり、より大きなサイズの新しいハッシュテーブルが作成され、データが古いハッシュテーブルから新しいハッシュテーブルに移行されます。

ハッシュテーブルのサイズのサイズ変更を調整します。

	void resize(int newCapacity)
	{
    
    
		Entry[] oldTable = table;
		int oldCapacity = oldTable.length;
		......
		//创建一个新的Hash Table
		Entry[] newTable = new Entry[newCapacity];
		//将Old Hash Table上的数据迁移到New Hash Table上
		transfer(newTable);
		table = newTable;
		threshold = (int)(newCapacity * loadFactor);
	}


table []配列の容量が小さい場合、ハッシュの衝突が発生する可能性が高いため、ハッシュテーブルのサイズと容量は非常に重要です。

一般的に、ハッシュテーブルコンテナに挿入するデータがある場合、容量が設定されたしきい値を超えているかどうかをチェックします。超えている場合は、ハッシュテーブルのサイズを増やす必要があります。このプロセスはサイズ変更と呼ばれます。
複数のスレッドが同時に新しい要素をHashMapに追加する場合、サイズ変更ごとに古いデータを新しいハッシュテーブルにマップする必要があるため、複数のサイズ変更では無限ループが発生する可能性があります。コードのこの部分はHashMapにあります。 #transfer()メソッド。次のように:

	void transfer(Entry[] newTable)
	{
    
    
		Entry[] src = table;
		int newCapacity = newTable.length;
		//下面这段代码的意思是:
		//  从OldTable里摘一个元素出来,然后放到NewTable中
		for (int j = 0; j < src.length; j++) {
    
    
			Entry<K,V> e = src[j];
			if (e != null) {
    
    
				src[j] = null;
				//以下代码是造成死循环的罪魁祸首。
				do {
    
    
					Entry<K,V> next = e.next;//取出第一个元素
					int i = indexFor(e.hash, newCapacity);
					e.next = newTable[i];
					newTable[i] = e;
					e = next;
				} while (e != null);
			}
		}
	}

3.グラフィカルなHashMap無限ループ:

通常のReHashプロセス(シングルスレッド):
ハッシュアルゴリズムは、キーmodを使用したテーブルのサイズ(つまり、配列の長さ)であると想定されています。
一番上のものは古いハッシュテーブルで、ハッシュテーブルのサイズは2であるため、mod2の後のキー= 3、7、5はすべてtable [1]で競合します。

次の3つのステップは、ハッシュテーブルのサイズを4に変更してから、すべての<key、value>を再ハッシュするプロセスです。

ここに画像の説明を挿入します

並行性の下での再ハッシュ(マルチスレッド)

2つのスレッドがあるとします。

	do {
    
    
		Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了,执行其他操作
		int i = indexFor(e.hash, newCapacity);
		e.next = newTable[i];
		newTable[i] = e;
		e = next;
	} while (e != null);

そして、スレッド2の実行が完了しました。したがって、次のようになります。

ここに画像の説明を挿入します
Thread1のeはkey(3)を指し、nextはkey(7)を指すため、スレッド2の再ハッシュ後、再編成されたスレッド2のリンクリストを指すことに注意してください。リンクリストの順序が逆になっていることがわかります。ここで、スレッド1は、スレッド2の操作後にHashMapになります。

2)スレッドが実行のために戻ってくるようにスケジュールされたら。
最初にnewTalbe [i] = e;
実行してからe = nextを実行すると、eはkey(7)をポイントし
、next = e.nextの次のサイクルはnextをkey(3)をポイントします。

ここに画像の説明を挿入します
3)すべてが大丈夫です。

スレッドは引き続き機能します。key(7)をピックオフし、それをnewTable [i]の最初のキーに配置し、eを下に移動します。この要素の位置にはすでに他の要素が格納されており、同じ位置にある要素はリンクリストの形式で格納され、新しく追加された要素はチェーンの先頭に配置され、以前に追加された要素は次のようになります。チェーンの最後に配置されます。

ここに画像の説明を挿入します

4)円形のリンクが表示されます。

e.next = newTable [i]により、key(3).nextがkey(7)をポイントします。
注:この時点で、key(7).nextはすでにkey(3)を指しており、循環リンクリストが表示されます。

ここに画像の説明を挿入します
したがって、スレッドがHashTable.get(11)を呼び出すと、悲劇が発生しました-無限ループ

JDK8の後->無限ループを解決するための拡張

JDK8は、HashMapの構造を変更し、データが少ない場合は元のリンクリスト部分をリンクリストに変更し、一定量を超えると赤黒木に変換します。ここでは主にリンクの違いについて説明します。リストと前のもの。

1. oldtabのサイズが2の場合、2> 2 * 0.75であるため、2つのノード7、3があります。次に、サイズ4のnewtableに展開してから、ノードをoldtableからnewtableに移動する必要があります。ここに2つのスレッドがあり、各スレッドにeがあり、次に現在のノードと次のノードをそれぞれ記録します。2つのスレッドが一緒に拡張されると、何かが発生します。

ここに画像の説明を挿入します
ここに画像の説明を挿入します

上記の分析では、新しいリンクリストの順序が古いリンクリストと完全に逆であるため、サイクルが生成されていることを見つけるのは難しくありません。新しいチェーンが元の順序で構築されている限り、サイクルは次のようになります。発生しません。

JDK8は、ヘッドとテールを使用して、リンクリストの順序が以前と同じになるようにし、循環参照が生成されないようにします。

概要:

jdk1.8の前後の違いは、jdk1.8の後、ノードはnewtable [j]のエンドノードに直接配置され、jdk1.8の前では、ヘッドノードに直接配置されることです。無限ループは解決されましたが、hashMapにはマルチスレッドでの使用に多くの問題があります。マルチスレッドモードでConcurrentHashMapを使用することをお勧めします。

おすすめ

転載: blog.csdn.net/m0_46405589/article/details/109206432