El principio subyacente de HashMap: estructura de datos + proceso put () + 2 elevado a la enésima potencia + bucle infinito + problema de cobertura de datos

navegación:

 

[Notas de Java + resumen de pasos a seguir] Conceptos básicos de Java + avanzado + JavaWeb + SSM + SpringBoot + comida para llevar de St. Regis + SpringCloud + turismo oscuro + centro comercial Guli + Xuecheng en línea + artículos avanzados de MySQL + modo de diseño + preguntas comunes de entrevistas + código fuente_vincewm Blog -Blog de CSDN

Tabla de contenido

1. La capa inferior

1.1 Estructura de datos HashMap

1.2 Mecanismo de expansión

1.3 proceso poner ()

1.4 ¿Cómo calcula HashMap la clave?

1.5 ¿Por qué la capacidad de HashMap 2 está elevada a la enésima potencia?

1.5.1 Razones

1.5.2 Demostración de hash uniforme de expansión: expansión de 2^4 a 2^5

2. Problemas de seguridad del hilo

2.1 ¿Es HashMap seguro para subprocesos? 

2.2 Soluciones seguras para subprocesos

2.3 Problema de bucle infinito durante la expansión JDK7

2.3.1 Demostración del problema del bucle infinito 

2.3.2 ¿Cómo resuelve JDK8 el problema del bucle infinito?

2.4 Problema de cobertura de datos durante la instalación de JDK8

2.5 Problema de autoincremento no atómico modCount


1. La capa inferior

1.1 Estructura de datos HashMap

En JDK1.7 y versiones anteriores, la capa inferior de HashMap es "matriz + lista enlazada unidireccional".

En JDK8, la capa inferior de HashMap se implementa mediante "matriz + lista enlazada unidireccional + árbol rojo-negro" El objetivo principal del uso del árbol rojo-negro es mejorar el rendimiento de las consultas. La matriz se usa para la búsqueda de hash, la lista vinculada se usa como método de dirección en cadena para manejar conflictos y el árbol rojo-negro reemplaza la lista vinculada con una longitud de 8.

1.2 Mecanismo de expansión

En HashMap, la capacidad inicial predeterminada de la matriz es 16 y esta capacidad se ampliará con un exponente de 2. Específicamente, HashMap se expandirá cuando los elementos de la matriz alcancen una determinada proporción, que se denomina factor de carga y el valor predeterminado es 0,75.

El mecanismo de expansión automática es para garantizar que HashMap no necesite ocupar demasiada memoria al principio y pueda garantizar suficiente espacio en tiempo real durante el uso. El uso de un exponente de 2 para la expansión es utilizar operaciones de bits para mejorar la eficiencia de las operaciones de expansión.

Cada elemento de la matriz almacena la dirección del nodo principal de la lista vinculada, y el método de dirección de enlace maneja los conflictos. Si la longitud de la lista vinculada llega a 8, el árbol rojo-negro reemplaza la lista vinculada.

1.3 proceso poner ()

Durante la ejecución del método put(), existen principalmente cuatro pasos:

  1. Calcule la posición de acceso a la clave y la operación hash & (2 ^ n-1), que en realidad es el resto del valor hash, y la eficiencia de la operación de bits es mayor.
  2. Al juzgar la matriz, si se encuentra vacía, expanda la capacidad a la capacidad inicial de 16 por primera vez.
  3. Determine el nodo principal de la posición de acceso a la matriz. Si se encuentra que el nodo principal está vacío, cree un nuevo nodo de lista vinculada y guárdelo en la matriz.
  4. Al juzgar el nodo principal de la posición de acceso a la matriz, si se encuentra que el nodo principal no está vacío, dependiendo de la situación, sobrescriba o inserte el elemento en la lista vinculada (método de inserción principal JDK7, método de inserción final JDK8), rojo -árbol negro.
  5. Después de insertar un elemento, juzgue el número de elementos y expanda la capacidad nuevamente con un índice de 2 si se encuentra que excede el umbral.

Entre ellos, el tercer paso se puede subdividir en los siguientes tres pequeños pasos:

1. Si la clave del elemento es la misma que la clave del nodo principal, el nodo principal se sobrescribirá directamente.

2. Si el elemento es un nodo de árbol, agregue el elemento al árbol.

3. Si el elemento es un nodo de lista vinculada, agregue el elemento a la lista vinculada. Después de agregar, es necesario juzgar la longitud de la lista vinculada para decidir si se convierte en un árbol rojo-negro. Si la longitud de la lista vinculada llega a 8 y la capacidad de la matriz no llega a 64, expanda la capacidad. Si la longitud de la lista vinculada llega a 8 y la capacidad de la matriz llega a 64, se convertirá en un árbol rojo-negro.

La tabla hash maneja conflictos: método de dirección abierta (detección lineal, detección secundaria, método de repetición de hash), método de dirección en cadena

1.4 ¿Cómo calcula HashMap la clave?

key=value&(2^n-1) #结果相当于value%(2^n),使用位运算只要是为了提高计算速度。

Por ejemplo, la capacidad actual de la matriz es 16 y queremos acceder a 18, entonces podemos usar 18&15==2. Equivale al 18%16==2.

En put(), calcula parte del código fuente de la clave:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 此处省略了代码
        // i = (n - 1) & hash]
        if ((p = tab[i = (n - 1) & hash]) == null)
            
            tab[i] = newNode(hash, key, value, null);
        
 
        else {
            // 省略了代码
        }
}

1.5 ¿Por qué la capacidad de HashMap 2 está elevada a la enésima potencia?

1.5.1 Razones

Calcule la operación Hash del valor correspondiente a la clave:

key=value&(2^n-1)#结果相当于value%(2^n)。例如18&15和18%16值是相等的

Los números binarios de 2^n-1 y 2^(n+1)-1 son iguales excepto por el primer dígito y los últimos dígitos. De esta manera, los elementos agregados se pueden distribuir uniformemente en cada posición del HashMap, evitando colisiones de hash.

Por ejemplo, el valor binario de 15 (es decir, 2^4-1) es 1111, el valor binario de 31 es 11111, el valor binario de 63 es 111111 y el valor binario de 127 es 1111111.

1.5.2 Demostración de hash uniforme de expansión: expansión de 2^4 a 2^5

0&(2^4-1)=0;0&(2^5-1)=0

16&(2^4-1)=0;16&(2^5-1)=16. Por lo tanto, después de la expansión, la posición de algunos valores cuya clave es 0 permanece sin cambios y algunos valores migran a las nuevas posiciones después de la expansión.

1&(2^4-1)=1;1&(2^5-1)=1

17&(2^4-1)=1; 17&(2^5-1)=17. Por lo tanto, después de la expansión, la posición de algunos valores cuya clave es 1 permanece sin cambios y algunos valores migran a las nuevas posiciones después de la expansión.

Demostrar expansión con resto:

Si cree que la operación AND es un poco difícil de entender, podemos usar el resto para demostrarlo:

Suponiendo una expansión de 16 a 32: 1%16=1, 17%16=1; 1%32=1, 17%32=17.

Las claves originales de 1 y 17 son ambas 1. Después de la expansión, la clave de 1 sigue siendo 1 y la clave de 17 se convierte en 17. De esta manera, el valor cuya clave original es 1 se aplica uniformemente en la tabla hash expandida (algunos valores permanecen sin cambios y algunos valores se mueven a la nueva posición después de la expansión).

2. Problemas de seguridad del hilo

2.1 ¿Es HashMap seguro para subprocesos? 

HashMap no es seguro para subprocesos y puede haber problemas de bucle infinito y problemas de cobertura de datos en un entorno de subprocesos múltiples.

En subprocesos múltiples, se recomienda utilizar la clase de herramienta Colecciones y el ConcurrentHashMap del paquete JUC.

2.2 Soluciones seguras para subprocesos

  • Utilice Hashtable (no se recomienda la API anterior)
  • Utilice la clase de herramienta Colecciones para envolver HashMap en un HashMap seguro para subprocesos.
    Collections.synchronizedMap(map);
  • Utilice el ConcurrentHashMap más seguro (recomendado), ConcurrentHashMap bloquea la ranura (nodo principal de la lista vinculada) para garantizar la seguridad del subproceso con menos rendimiento.
  • Después de usar sincronizado o Lock para bloquear HashMap, la operación es equivalente a la ejecución de cola multiproceso (es más problemática y no se recomienda).

2.3 Problema de bucle infinito durante la expansión JDK7

2.3.1 Demostración del problema del bucle infinito 

Proceso de expansión de un solo subproceso:

En JDK7, el método de dirección de cadena HashMap adopta el método de inserción de encabezado cuando se trata de conflictos, y el método de inserción de encabezado todavía se usa cuando se expande la capacidad, por lo que el orden de los nodos en la lista vinculada se invertirá.

Si hay dos subprocesos T1 y T2 expandiendo una lista vinculada al mismo tiempo, ambos marcan el nodo principal y el segundo nodo. En este momento, T2 está bloqueado. Después de que T1 ejecuta la expansión, el orden de los nodos de la lista vinculada es invertido. Voltear generará una lista enlazada circular, es decir, B.next=A; A.next=B, por lo tanto, un bucle infinito.

2.3.2 ¿Cómo resuelve JDK8 el problema del bucle infinito?

El método de inserción de cola JDK8 resuelve el problema del bucle infinito.

En JDK8, HashMap adopta el método de inserción de cola y la posición de los nodos de la lista vinculada no se invertirá durante la expansión, lo que resuelve el problema del bucle de expansión infinito, pero el rendimiento es un poco peor porque es necesario atravesar la lista vinculada para encontrar la cola. 

Por ejemplo, es necesario migrar A——>B——>C. Al migrar, primero mueva el nodo principal A, luego mueva B e insértelo en la cola de A, y luego mueva C para insertar la cola, de modo que el El resultado sigue siendo A——>B——>C. El orden no ha cambiado y el hilo de expansión

2.4 Problema de cobertura de datos durante la instalación de JDK8

HashMap no es seguro para subprocesos. Si los datos insertados por dos subprocesos simultáneos son iguales después del resto del hash, es posible que se sobrescriban los datos.

El hilo A se bloquea cuando encuentra la posición nula de la lista vinculada y está listo para insertarse, y luego el hilo B encuentra la posición nula y la inserta con éxito. Con la recuperación del hilo A, debido a que se ha considerado nulo, sobrescribe e inserta directamente esta posición y sobrescribe los datos insertados por el hilo B.

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)     // 如果没有 hash 碰撞,则直接插入
            tab[i] = newNode(hash, key, value, null);
    }

2.5 Problema de autoincremento no atómico modCount

modCount: variable miembro de HashMap, utilizada para registrar la cantidad de veces que se ha modificado HashMap

put ejecutará la operación modCount ++, que se divide en leer, agregar y guardar. No es una operación atómica y también habrá problemas de seguridad de subprocesos. 

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
//put会执行modCount++操作,这步操作分为读取、增加、保存,不是一个原子性操作,也会出现线程安全问题。 
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

Supongo que te gusta

Origin blog.csdn.net/qq_40991313/article/details/131620721
Recomendado
Clasificación