Explicación detallada del problema de bucle infinito de HashMap en subprocesos múltiples

Hola a todos, vi una pregunta muy interesante cuando estaba leyendo el blog técnico hoy, como muestra el título ------ "En el caso de multi-threading, con respecto al bucle infinito de HashMap, recuerden que estoy cuando Primero aprendí JavaSE, vi este problema. En ese momento, la reserva de conocimiento no era suficiente y no lo estudié en profundidad. Hoy hablaré de ello en detalle, y espero poder ayudarlos y avanzar juntos.

Inicio del texto:

HashMap de Java no es seguro para subprocesos. ConcurrentHashMap debe usarse en subprocesos múltiples.

[HashMap] problema en subprocesos múltiples (hablando principalmente sobre el problema del bucle infinito aquí):

1. Después de la operación put de subprocesos múltiples, la operación get provoca un bucle infinito.
2. Después de poner un elemento no NULL de subprocesos múltiples, la operación get obtendrá el valor NULL.
3. La operación put de subprocesos múltiples provoca la pérdida del elemento.

1. ¿Por qué aparece un bucle sin fin en la versión Jdk7?

( Al usar HashMap no seguro para subprocesos múltiples en subprocesos múltiples, el subproceso único no aparecerá en absoluto)

HashMap utiliza una lista vinculada para resolver conflictos de Hash. Debido a que es una estructura de lista vinculada, es fácil formar un vínculo cerrado, por lo que siempre que un hilo realice una operación de obtención en este HashMap durante el ciclo, se producirá un ciclo sin fin.

En el caso de un solo hilo, solo un hilo opera en la estructura de datos del HashMap, y es imposible producir un ciclo cerrado.

Eso solo sucedería en el caso de concurrencia multiproceso, es decir, cuando la operación put.
Si size> initialCapacity * loadFactor, entonces el HashMap realizará una operación de refrito y la estructura del HashMap cambiará drásticamente. Es muy probable que los dos subprocesos desencadenaron la operación de refrito en este momento, lo que resultó en un ciclo cerrado.

2. ¿Cómo sucedió?

Almacenar datos 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;
	}

Cuando colocamos un elemento en el HashMap, primero obtenemos la posición (es decir, el subíndice) de este elemento en la matriz de acuerdo con el valor hash de la clave, y luego podemos colocar este elemento en la posición correspondiente.
Si ya hay otros elementos almacenados en la ubicación de este elemento, entonces los elementos en la misma posición se almacenarán en forma de una lista vinculada, con los elementos recién agregados al principio de la cadena y los elementos agregados previamente al final. de la cadena.

Compruebe si la capacidad supera el addEntry estándar:

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

Si el tamaño ahora excede el umbral, entonces se requiere una operación de cambio de tamaño, se crea una nueva tabla hash con un tamaño mayor y los datos se migran de la tabla Hash anterior a la nueva tabla Hash.

Ajuste el tamaño de la tabla hash:

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


Cuando la capacidad de la matriz table [] es pequeña, es probable que se produzcan colisiones hash, por lo que el tamaño y la capacidad de la tabla Hash son muy importantes.

En general, cuando el contenedor de la tabla Hash tiene datos para insertar, verificará si la capacidad excede el umbral establecido. Si lo excede, es necesario aumentar el tamaño de la tabla Hash. Este proceso se llama resize.
Cuando varios subprocesos agregan nuevos elementos al HashMap al mismo tiempo, varios cambios de tamaño tendrán una cierta probabilidad de un bucle infinito, porque cada cambio de tamaño debe asignar los datos antiguos a la nueva tabla hash. Esta parte del código está en el HashMap #transfer () método. de la siguiente manera:

	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. Bucle infinito gráfico HashMap:

Proceso normal de ReHash (hilo único): se
supone que nuestro algoritmo hash es simplemente el tamaño de la tabla con la clave mod (es decir, la longitud de la matriz).
La superior es la antigua tabla hash, en la que el tamaño de la tabla hash es 2, por lo que la clave = 3, 7, 5, después del mod 2, todos entran en conflicto en la tabla [1].

Los siguientes tres pasos son el proceso de cambiar el tamaño de la tabla Hash a 4, y luego repetir todas las <clave, valor>.

Inserte la descripción de la imagen aquí

Rehash bajo concurrencia (multiproceso)

Supongamos que tenemos dos hilos.

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

Y nuestra ejecución del hilo dos está completa. Entonces tenemos el siguiente aspecto:

Inserte la descripción de la imagen aquí
Tenga en cuenta que debido a que la e de Thread1 apunta a la tecla (3) y la siguiente apunta a la tecla (7), después del refrito del hilo dos, apunta a la lista enlazada del hilo dos reorganizado. Podemos ver que el orden de la lista enlazada se invierte. Aquí, el hilo uno se convierte en HashMap después de la operación del hilo dos.

2) Una vez que el hilo está programado para volver a ejecutarse.
Primero, ejecute newTalbe [i] = e;
y luego e = next, lo que hace que e apunte a la tecla (7),
y el siguiente ciclo de next = e.next hace que next apunte a la tecla (3).

Inserte la descripción de la imagen aquí
3) Todo está bien.

El hilo sigue funcionando. Elija la tecla (7), colóquela en la primera de la nueva tabla [i], mueva ey la siguiente hacia abajo. Ya existen otros elementos almacenados en la posición de este elemento, luego los elementos en la misma posición se almacenarán en forma de lista enlazada, los recién agregados se colocan en la cabecera de la cadena, y los previamente agregados son colocado al final de la cadena.

Inserte la descripción de la imagen aquí

4) Aparece el enlace circular.

e.next = newTable [i] hace que la tecla (3) .next apunte a la tecla (7).
Nota: En este momento, la tecla (7) .next ya apunta a la tecla (3), y aparece la lista enlazada circular.

Inserte la descripción de la imagen aquí
Entonces, cuando nuestro hilo se llamó HashTable.get (11), apareció la tragedia: Infinite Loop .

Después de JDK8 -> Expansión para resolver el bucle infinito

JDK8 modificó la estructura de HashMap y cambió la parte de la lista vinculada original a la lista vinculada cuando hay menos datos. Cuando excede una cierta cantidad, se transforma en un árbol rojo-negro. Aquí discutimos principalmente la diferencia entre lista y la anterior.

1. Si el tamaño de oldtab es 2, hay dos nodos 7, 3, porque 2> 2 * 0.75, ahora necesitamos expandir a una nueva tabla de tamaño 4, y luego mover el nodo de oldtable a newtable, asumiendo que hay dos hilos aquí, hay e en cada hilo, luego registra el nodo actual y el siguiente nodo respectivamente, y ahora los dos hilos se expanden juntos, algo sucederá

Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí

A través del análisis anterior, no es difícil encontrar que el ciclo se genera porque el orden de la nueva lista enlazada es completamente opuesto a la lista enlazada anterior, por lo que siempre que la nueva cadena se construya en el orden original, el ciclo será no ocurrió.

JDK8 utiliza la cabeza y la cola para garantizar que el orden de la lista enlazada sea el mismo que antes, de modo que no se generen referencias circulares.

resumen:

La diferencia entre antes y después de jdk1.8 es que después de jdk1.8, el nodo se coloca directamente en el nodo final de newtable [j], mientras que antes de jdk1.8, se coloca directamente en el nodo principal. Aunque el bucle infinito está resuelto, hashMap todavía tiene muchos problemas en el uso de múltiples subprocesos. Es mejor usar ConcurrentHashMap en modo de múltiples subprocesos.

Supongo que te gusta

Origin blog.csdn.net/m0_46405589/article/details/109206432
Recomendado
Clasificación