Detaillierte Erläuterung des HashMap-Endlosschleifenproblems unter Multithreading

Hallo allerseits, ich habe heute beim Lesen des technischen Blogs eine sehr interessante Frage gesehen, wie der Titel zeigt ------ "Denken Sie beim Multithreading in Bezug auf die Endlosschleife von HashMap daran, dass ich bin, wenn ich bin Als ich JavaSE zum ersten Mal lernte, sah ich dieses Problem. Zu dieser Zeit reichte die Wissensreserve nicht aus und ich habe sie nicht gründlich studiert. Heute werde ich ausführlich darüber sprechen und hoffe, Ihnen zu helfen und gemeinsam Fortschritte zu erzielen.

Textanfang:

Javas HashMap ist nicht threadsicher. ConcurrentHashMap sollte beim Multithreading verwendet werden.

[HashMap] -Problem unter Multithreading (hauptsächlich über das Endlosschleifenproblem hier):

1. Nach der Multithread-Put-Operation verursacht die Get-Operation eine Endlosschleife.
2. Nach dem Multithread-Put-Nicht-NULL-Element erhält die get-Operation den NULL-Wert.
3. Multithread-Put-Vorgang verursacht Elementverlust.

1. Warum erscheint in der Jdk7-Version eine Endlosschleife?

( Wenn Sie nicht threadsichere HashMap unter Multithreading verwenden, wird Single-Threading überhaupt nicht angezeigt.)

HashMap verwendet eine verknüpfte Liste, um Hash-Konflikte zu lösen. Da es sich um eine verknüpfte Listenstruktur handelt, ist es einfach, eine geschlossene Verknüpfung zu bilden. Solange ein Thread während der Schleife eine Abrufoperation für diese HashMap ausführt, tritt eine Endlosschleife auf.

Bei einem einzelnen Thread arbeitet nur ein Thread mit der Datenstruktur der HashMap, und es ist unmöglich, eine geschlossene Schleife zu erzeugen.

Dies würde nur bei Multithread-Parallelität geschehen, dh bei der Put-Operation.
Wenn size> initialCapacity * loadFactor, führt die HashMap einen Wiederaufbereitungsvorgang durch und die Struktur der HashMap ändert sich drastisch. Es ist sehr wahrscheinlich, dass die beiden Threads zu diesem Zeitpunkt den Wiederaufbereitungsvorgang ausgelöst haben, was zu einer geschlossenen Schleife führte.

2. Wie ist es passiert:

Daten speichern 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;
	}

Wenn wir ein Element in die HashMap einfügen, erhalten wir zuerst die Position (nämlich den Index) dieses Elements im Array gemäß dem Hashwert des Schlüssels, und dann können wir dieses Element an der entsprechenden Position platzieren.
Wenn an der Position dieses Elements bereits andere Elemente gespeichert sind, werden die Elemente an derselben Position in Form einer verknüpften Liste mit neu hinzugefügten Elementen am Kopf der Kette und zuvor hinzugefügten Elementen am Ende gespeichert der Kette.

Überprüfen Sie, ob die Kapazität den Standard-AddEntry überschreitet:

	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);
	}

Wenn die Größe jetzt den Schwellenwert überschreitet, ist eine Größenänderungsoperation erforderlich, eine neue Hash-Tabelle mit einer größeren Größe wird erstellt und die Daten werden von der alten Hash-Tabelle in die neue Hash-Tabelle migriert.

Passen Sie die Größe der Hash-Tabellengröße an:

	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);
	}


Wenn die Kapazität des table [] -Arrays klein ist, können Hash-Kollisionen auftreten. Daher sind Größe und Kapazität der Hash-Tabelle sehr wichtig.

Wenn im Hash-Tabellencontainer Daten eingefügt werden müssen, wird im Allgemeinen geprüft, ob die Kapazität den festgelegten Schwellenwert überschreitet. Wenn sie überschritten wird, muss die Größe der Hash-Tabelle erhöht werden. Dieser Vorgang wird als Größenänderung bezeichnet.
Wenn mehrere Threads gleichzeitig neue Elemente zur HashMap hinzufügen, besteht bei mehreren Größenänderungen eine gewisse Wahrscheinlichkeit für eine Endlosschleife, da bei jeder Größenänderung die alten Daten der neuen Hash-Tabelle zugeordnet werden müssen. Dieser Teil des Codes befindet sich in der HashMap # transfer () -Methode wie folgt:

	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. Grafische HashMap-Endlosschleife:

Normaler ReHash-Prozess (einzelner Thread): Es wird
angenommen, dass unser Hash-Algorithmus einfach die Größe der Tabelle mit dem Schlüsselmod ( dh die Länge des Arrays) ist.
Die oberste ist die alte Hash-Tabelle, in der die Größe der Hash-Tabelle 2 ist, also der Schlüssel = 3, 7, 5, nach Mod 2 alle Konflikte in Tabelle [1].

In den nächsten drei Schritten wird die Größe der Hash-Tabelle auf 4 geändert und anschließend alle <Schlüssel, Wert> erneut aufbereitet.

Fügen Sie hier eine Bildbeschreibung ein

Aufwärmen unter Parallelität (Multithread)

Angenommen, wir haben zwei Threads.

	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);

Und unsere Thread-2-Ausführung ist abgeschlossen. Wir sehen also folgendermaßen aus:

Fügen Sie hier eine Bildbeschreibung ein
Beachten Sie, dass das e von Thread1 auf Schlüssel (3) und das nächste auf Schlüssel (7) zeigt. Nach dem erneuten Aufbereiten von Thread 2 zeigt es auf die verknüpfte Liste von Thread 2, die neu organisiert wurde. Wir können sehen, dass die Reihenfolge der verknüpften Liste umgekehrt ist. Hier wird Thread eins nach der Operation von Thread zwei zur HashMap.

2) Sobald geplant ist, dass der Thread zur Ausführung zurückkommt.
Führen Sie zuerst newTalbe [i] = e
und dann e = next aus, wodurch e auf Schlüssel (7) zeigt,
und der nächste Zyklus von next = e.next bewirkt, dass next auf Schlüssel (3) zeigt.

Fügen Sie hier eine Bildbeschreibung ein
3) Alles ist in Ordnung.

Der Thread funktioniert weiter. Nimm die Taste (7), lege sie in die erste von newTable [i] und bewege e und die nächste nach unten. Es sind bereits andere Elemente an der Position dieses Elements gespeichert, dann werden die Elemente an derselben Position in Form einer verknüpften Liste gespeichert, die neu hinzugefügten werden am Kopf der Kette platziert und die zuvor hinzugefügten am Ende der Kette platziert.

Fügen Sie hier eine Bildbeschreibung ein

4) Der kreisförmige Link wird angezeigt.

e.next = newTable [i] bewirkt, dass Schlüssel (3) .next auf Schlüssel (7) zeigt.
Hinweis: Zu diesem Zeitpunkt zeigt Schlüssel (7) .next bereits auf Schlüssel (3), und die zirkuläre verknüpfte Liste wird angezeigt.

Fügen Sie hier eine Bildbeschreibung ein
Also, wenn unser Faden HashTable.get (11) genannt, appeared- die Tragödie Endlosschleife .

Nach JDK8 -> Erweiterung zur Lösung der Endlosschleife

JDK8 hat die Struktur von HashMap geändert und den ursprünglichen Teil der verknüpften Liste in die verknüpfte Liste geändert, wenn weniger Daten vorhanden sind. Wenn eine bestimmte Menge überschritten wird, wird sie in einen rot-schwarzen Baum umgewandelt. Hier wird hauptsächlich der Unterschied zwischen den verknüpften Listen erläutert Liste und die vorherige.

1. Wenn die Größe von oldtab 2 ist, gibt es zwei Knoten 7, 3, da 2> 2 * 0,75. Jetzt müssen wir auf newtable der Größe 4 erweitern und dann den Knoten von oldtable zu newtable verschieben, vorausgesetzt, es gibt zwei Threads hier, Es gibt e in jedem Thread, der nächste zeichnet den aktuellen Knoten bzw. den nächsten Knoten auf, und jetzt erweitern sich die beiden Threads zusammen, etwas wird passieren

Fügen Sie hier eine Bildbeschreibung ein
Fügen Sie hier eine Bildbeschreibung ein

Durch die obige Analyse ist es nicht schwierig festzustellen, dass der Zyklus generiert wird, da die Reihenfolge der neuen verknüpften Liste der alten verknüpften Liste völlig entgegengesetzt ist. Solange die neue Kette in der ursprünglichen Reihenfolge erstellt wird, wird der Zyklus ausgeführt nicht auftreten.

JDK8 verwendet Kopf und Schwanz, um sicherzustellen, dass die Reihenfolge der verknüpften Liste dieselbe ist wie zuvor, sodass keine Zirkelverweise generiert werden.

Zusammenfassung:

Der Unterschied zwischen vor und nach jdk1.8 besteht darin, dass der Knoten nach jdk1.8 direkt am Endknoten von newtable [j] platziert wird, während er vor jdk1.8 direkt auf dem Kopfknoten platziert wird. Obwohl die Endlosschleife gelöst ist, hat hashMap immer noch viele Probleme bei der Verwendung von Multithreads. Es ist besser, ConcurrentHashMap im Multithread-Modus zu verwenden.

Ich denke du magst

Origin blog.csdn.net/m0_46405589/article/details/109206432
Empfohlen
Rangfolge