Reconocer HashMap

La estructura y los principios subyacentes de HashMap:

HashMap es una estructura de datos muy utilizada, que es una estructura de datos compuesta por  una matriz y una lista vinculada .

Aproximadamente de la siguiente manera, cada lugar en la matriz almacena una instancia de Key-Value, que se llama Entry en Java7 y Node en Java8.

Debido a que todas sus posiciones son nulas, se calculará un valor de índice de acuerdo con el algoritmo hash de la clave cuando se inserte la opción put. Por ejemplo, si puse ("Shuaibing", 220), inserté un elemento llamado "Hola". En este momento, calcularemos el índice de posición insertado a través del algoritmo hash, y el índice calculado es 2. El resultado es como sigue:

hash("guapo") = 2

Justo ahora mencionamos que también hay una lista enlazada, ¿por qué necesitamos una lista enlazada y cómo es una lista enlazada?

Sabemos que la longitud de la matriz es limitada. En la longitud limitada, usamos hash, y el hash en sí es probabilístico, es decir, los valores de hash de Shuai B y B Shuai tienen cierta probabilidad de ser iguales:

hash ("Bing Shuai") = 2

Luego se forma una lista enlazada como esta:

Cada nodo de Nodo guardará su propio hash, clave, valor y el siguiente nodo de Nodo. ​​El código fuente de Nodo es el siguiente:

Hablando de la lista vinculada, ¿sabe cómo se inserta el nuevo nodo de entrada (Nodo) cuando se inserta en la lista vinculada?

Antes de Java 8, se usaba el método de inserción de encabezado , lo que significa que el nuevo valor reemplazará el valor original del encabezado, y el valor original se enviará a la lista vinculada, como en el ejemplo anterior, porque el diseñador piensa que el El valor más reciente se reemplazará más adelante. La posibilidad de buscar es un poco más, y también es para mejorar la eficiencia de la búsqueda.

Sin embargo, después de java8, se cambió a inserción de cola.

Entonces, ¿por qué cambiarlo a inserción de cola?

Primero, veamos el mecanismo de expansión de HashMap :

Como se mencionó anteriormente, la capacidad de la matriz es limitada.Después de que los datos se inserten varias veces, se expandirán cuando alcancen una cierta cantidad , es decir, cambien de tamaño .

¿Cuándo es el momento para la expansión de cambio de tamaño?

Hay dos factores:

  • Capacidad : la longitud actual del HashMap.
  • LoadFactor : factor de carga, el valor predeterminado es 0.75f.

¿Cómo lo entiende? Por ejemplo, la capacidad actual de la matriz es 100. Cuando guarda la matriz 76, se considera que ha alcanzado el valor nominal de LoadFactor. En este momento, es necesario cambiar el tamaño, así que expanda la capacidad , pero la expansión de HashMap Capacity no es tan simple como simplemente expandir la capacidad.

¿Expansión? ¿Cómo se expande?

Dividido en dos pasos:

  • Expansión : crea una nueva matriz vacía de entrada con el doble de longitud que la matriz original .
  • ReHash : recorre la matriz de entrada original y vuelve a codificar toda la entrada en la nueva matriz.

¿Por qué quieres volver a hacer hash, no es bueno copiar el pasado directamente?

Es porque después de expandir la longitud de la nueva matriz de Entrada, las reglas de Hash también cambiarán en consecuencia.

Fórmula hash : índice = HashCode (Clave) & (Longitud - 1)

La longitud original de la matriz (longitud) es 8, el valor obtenido por su operación de bits es 2, la nueva longitud de la matriz es 16 y el valor obtenido por su operación de bits es obviamente diferente.

Antes de la expansión:

Después de la expansión:

Después de hablar sobre el mecanismo de expansión, respondamos la pregunta anterior, ¿por qué usó el método de inserción de cabeza antes, pero lo cambió al método de inserción de cola después de java8?

Por ejemplo:

Ahora queremos usar diferentes subprocesos para insertar A, B y C en el contenedor con una capacidad de 2. Si establecemos un punto de interrupción antes de cambiar el tamaño, significa que los datos se han insertado, pero el cambio de tamaño no se ha ejecutado, por lo que puede ser antes de la expansión Tales:

Podemos ver que la lista enlazada apunta a A->B->C:

Usando el método de inserción de encabezado de la lista enlazada única, los elementos nuevos en la misma posición siempre se colocarán en el encabezado de la lista enlazada. Los elementos en la misma cadena de entrada en la matriz anterior pueden colocarse en diferentes posiciones de la nueva matriz después de volver a calcular la posición del índice:

Es posible que el siguiente puntero de B apunte a A:

Una vez que se ajustan varios hilos, puede aparecer una lista circular enlazada :

Si va a obtener el valor en este momento, aparecerá una tragedia: bucle infinito.

El ejemplo anterior es para ilustrar el método de inserción de encabezado antes de 1.7 En el escenario concurrente, hay un problema fatal, es decir, se puede formar un bucle de datos y se produce un bucle infinito al obtener datos. ( Aunque HashMap no es seguro para subprocesos )
Antes de 1.8, la forma de lidiar con los conflictos de hash es usar listas vinculadas para almacenar datos para resolver y usar el método de inserción de cabeza para mejorar cierta eficiencia .
Pero después de 1.8, esta mejora de eficiencia es prescindible, debido a que la longitud de la lista enlazada supera los 7, es necesario considerar actualizar el árbol rojo-negro, por lo que incluso si el número de recorridos de inserción de cola es limitado, la eficiencia no será mucho. afectado.
En segundo lugar, debido a los cambios en la estructura de datos después de 1.8, cuando la longitud de la lista enlazada alcanza el umbral, el método de interpolación principal no es aplicable después de actualizar a un árbol rojo-negro, porque la construcción de un árbol rojo-negro necesita para comparar y actualizar la secuencia, por lo que no se puede llamar el método de interpolación de cabeza.

El uso de la inserción de encabezado cambiará el orden en la lista enlazada, pero si se usa la inserción de cola , el orden original de los elementos de la lista enlazada se mantendrá durante la expansión y no habrá problema de que la lista enlazada forme un bucle.

Es decir, originalmente era A->B, pero la lista enlazada sigue siendo A->B después de la expansión:

Java 1.7 puede causar un bucle infinito cuando se opera HashMap con múltiples subprocesos. La razón es que el orden de las listas enlazadas delantera y trasera se invierte después de la expansión y la transferencia, y la relación de referencia de los nodos en la lista enlazada original se modifica durante el proceso de transferencia.

Bajo la misma premisa, Java 1.8 no causará un bucle infinito, la razón es que el orden de la lista enlazada permanece sin cambios después de la expansión y transferencia, y se mantiene la relación de referencia de los nodos anteriores.

¿Significa eso que Java8 puede usar HashMap en subprocesos múltiples?

Creo que incluso si no habrá un bucle infinito, pero a través del código fuente, puedo ver que el método put/get no tiene un bloqueo de sincronización. Lo más probable que suceda en una situación de subprocesos múltiples es: el valor no se puede garantizar el valor de la entrada en el último segundo, y el valor de la obtención en el segundo siguiente seguirá siendo el mismo, por lo que la seguridad de subprocesos aún no está garantizada.

Entonces, ¿cuál es la longitud de inicialización predeterminada de HashMap?

Recuerdo que el tamaño inicial era 16 cuando miré el código fuente.

Entonces, ¿por qué es 16?

En la línea 236 de JDK1.8, hay 1<<4, que es 16.

¿Por qué usar operaciones de bits? ¿No es bueno usar 16 directamente?

La razón principal es que el rendimiento de las operaciones con bits es bueno , y la eficiencia de las operaciones con bits es mucho mayor que la de los cálculos aritméticos. ¿ Por qué el rendimiento de las operaciones con bits es tan bueno? Es porque las operaciones con bits operan directamente en la memoria y no No es necesario realizar una conversión binaria Debe saber que las computadoras usan binarios en forma de almacenamiento de datos.

Entonces, ¿por qué usar 16 en lugar de otros números?

Para saber por qué es 16, tenemos que fijarnos en las características de almacenamiento de datos de HashMap.

Por ejemplo , si la clave es "Shuaibing", el valor decimal es 766132 y el valor binario es 10111011000010110100.

Veamos de nuevo la fórmula de cálculo del índice:

Fórmula hash: índice = HashCode (Clave) & (Longitud- 1)

 Así, índice = 1011101100001011 0100 & (16 -1);

 Por lo tanto, índice = 1011101100001011 0100 & 15;

Y 15 es 1111 en binario, entonces:

índice = 1011101100001011 0100 y 1111 =  0100   = 4。

La razón para usar operaciones bit AND es que el efecto es el mismo que el del módulo, ¡y el rendimiento mejorará mucho!

Se puede decir que el resultado final del índice obtenido por el algoritmo Hash depende completamente de los últimos dígitos del valor Hashcode (binario) de la Clave .

Y debido a que al usar  un número que es una potencia de 2 como tamaño inicial, todos los bits binarios del valor de (Length-1) son 1, en este caso, el resultado de index es equivalente a los últimos dígitos de HashCode ( binario) valor.

Por lo tanto, siempre que el HashCode de entrada se distribuya uniformemente, se puede obtener un hash distribuido uniformemente En otras palabras, se pueden .

Entonces, ¿por qué es 16? ¿No es una potencia entera de 2? En teoría está bien, pero si son 2, 4 u 8, será un poco pequeño, y se ampliará la capacidad sin agregar muchos datos, es decir, se ampliará la capacidad con frecuencia, lo que afectará el rendimiento. ¿Por qué no 32 o más, entonces no es una pérdida de espacio, por lo que 16 está reservado como un valor de experiencia muy adecuado?

Otra pregunta, ¿por qué necesitamos reescribir el método hashCode cuando reescribimos el método equals?

¿Por favor dame un ejemplo usando HashMap?

Porque en java, todos los objetos se heredan de la clase Object. Hay dos métodos equals y hashCode en la clase Ojbect , los cuales se usan para comparar si dos objetos son iguales.

Cuando el método equals no se reescribe, heredamos la implementación predeterminada de equals in object. El equals interno es para comparar las direcciones de memoria de dos objetos . Obviamente, hemos creado dos objetos nuevos y sus direcciones de memoria deben ser diferentes.

  • Para objetos de valor, == compara los valores de dos objetos
  • Para objetos de referencia, las direcciones de memoria de los dos objetos se comparan

Las características del código hash son:

Para el mismo objeto, si no ha sido modificado (use equals para devolver verdadero), entonces su valor de código hash es el mismo siempre que sea

Para dos objetos, si sus iguales devuelven falso, entonces sus valores de código hash también pueden ser iguales

Al agregar datos a HashMap, hay dos situaciones:

  • El lugar donde el índice de matriz actual está vacío, esta situación es muy simple, simplemente coloque el elemento directamente.
  • Ya hay un elemento que ocupa la posición del índice. En este caso, debemos juzgar si el elemento en esta posición es igual al elemento actual y usar iguales para comparar.

Si se utiliza la regla predeterminada, se comparan las direcciones de los dos objetos. Es decir, los dos deben ser el mismo objeto para ser iguales. Por supuesto, también podemos reescribir el método equals para implementar nuestras propias reglas de comparación. La más común es juzgar si son iguales comparando los valores de los atributos .

Si los dos son iguales, se sobrescribirán directamente, si no son iguales, el elemento se almacenará en la estructura de la lista enlazada debajo del elemento original.

Entonces, cuando usamos un objeto personalizado como clave, ¿por qué necesitamos reescribir el método hashCode mientras reescribimos el método equals?

Tomemos un ejemplo para ver ( desde la perspectiva de Hashmap ), qué tipo de problemas ocurrirán si se reescribe equals sin reescribir el código hash:

public class MyTest {
    private static class Person{
        int idCard;
        String name;
        public Person(int idCard, String name) {
            this.idCard = idCard;
            this.name = name;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()){
                return false;
            }
            Person person = (Person) o;
            //两个对象是否等值,通过idCard来确定
            return this.idCard == person.idCard;
        }
    }
    public static void main(String []args){
        HashMap<Person,String> map = new HashMap<Person, String>();
        Person person = new Person(1234,"乔峰");
        //put到hashmap中去
        map.put(person,"天龙八部");
        //get取出,从逻辑上讲应该能输出“天龙八部”
        System.out.println("结果:"+map.get(new Person(1234,"萧峰")));
    }
}

实际输出结果:null

Si ya tenemos una cierta comprensión del principio de HashMap, este resultado no es difícil de entender. Aunque las claves que usamos son lógicamente equivalentes (comparación de igual a igual) al realizar operaciones de obtención y colocación, dado que el método hashCode no se reescribe, cuando se realiza la operación de colocación, key(hashcode1)–> hash–>indexFor–>final index position y key(hashcode2)–>hash–>indexFor–>posición de índice final al extraer el valor a través de key, porque hashcode1 no es igual a hashcode2, no localiza una posición de matriz y devuelve lógicamente el valor incorrecto es nulo.

Por lo tanto, al reescribir el método equals, debe prestar atención a reescribir el método hashCode ; al mismo tiempo, también debe asegurarse de que dos objetos que se consideren iguales por equals devuelvan el mismo valor entero al llamar al método hashCode.

¿Cuándo se convierte la lista vinculada en HashMap en jdk1.8 en un árbol rojo-negro?

Cuando el tamaño de la lista enlazada en Hashmap supere los ocho, se convertirá automáticamente en un árbol rojo-negro, y cuando la eliminación sea inferior a seis, volverá a convertirse en una lista enlazada.

Otra pregunta, si HashMap no es seguro para subprocesos, ¿puede hablarme sobre cómo maneja HashMap en escenarios seguros para subprocesos?

En tales escenarios, generalmente usamos HashTable o ConcurrentHashMap , pero debido a la concurrencia del primero , básicamente no hay escenarios de uso, por lo que todos usamos ConcurrentHashMap en escenarios donde los subprocesos no son seguros. He leído el código fuente de HashTable, es muy simple y grosero, está bloqueado directamente en el método, la concurrencia es muy baja y, como máximo, se permite el acceso de un subproceso al mismo tiempo, ConcurrentHashMap es mucho mejor, hay hay una gran diferencia entre 1.7 y 1.8, pero la concurrencia es mayor que la anterior es mucho mejor.

Reescribir es igual desde la perspectiva de Set debe reescribir hashCode

Háblame de ConcurrentHashMap?

En comparación con Hashtable, ConcurrentHashMap tiene un mayor grado de simultaneidad, por lo que, en general, en las operaciones de subprocesos múltiples, se selecciona básicamente ConcurrentHashMap.

¿Hablemos primero de Hashtable?

En comparación con HashMap, Hashtable es seguro para subprocesos y adecuado para su uso en situaciones de subprocesos múltiples, pero la eficiencia no es optimista.

Cuéntame sobre la baja eficiencia de HashTable.

En el código fuente, se bloqueará cuando opere con datos, por lo que la eficiencia es relativamente baja.

Además de esto, ¿hay alguna diferencia entre Hashtable y HashMap?

Hashtable no permite que la clave o el valor sean nulos, mientras que el valor de la clave de HashMap puede ser nulo.

¿Por qué Hashtable no permite que KEY y VALUE sean nulos, pero HashMap sí?

Porque Hashtable arrojará directamente una excepción de puntero nulo cuando ponemos un valor nulo, pero HashMap ha realizado un procesamiento especial:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

Parece que todavía no está claro por qué Hashtable no permite que la clave o el valor sean nulos, pero el valor clave de HashMap puede ser nulo.

Esto se debe a que Hashtable utiliza un mecanismo a prueba de fallas (fail-safe) , que hace que los datos que lea esta vez no sean necesariamente los datos más recientes.

Si usa un valor nulo, será imposible juzgar si la clave correspondiente no existe o está vacía, porque no puede volver a llamar a container(key) para juzgar si la clave existe. ConcurrentHashMap es lo mismo.

¿Hay alguna diferencia?

  • La implementación es diferente : Hashtable hereda la clase Dictionary, mientras que HashMap hereda la clase AbstractMap.

    Parece que nadie ha usado este Diccionario, y yo tampoco.

  • La capacidad inicial es diferente : la capacidad inicial de HashMap es: 16 , la capacidad inicial de Hashtable es: 11 y el factor de carga predeterminado de ambos es: 0,75.

  • El mecanismo de expansión es diferente : cuando la capacidad existente es mayor que la capacidad total * factor de carga, la regla de expansión HashMap es 2 veces la capacidad actual y la regla de expansión Hashtable es 2 veces la capacidad actual + 1.

  • Los iteradores son diferentes : el iterador Iterator en HashMap es rápido, mientras que el Enumerador de Hashtable no lo es.

    Por lo tanto, cuando otros subprocesos cambien la estructura de HashMap, como agregar o eliminar elementos, se lanzará ConcurrentModificationException, pero no Hashtable.

¿Qué es fallar rápido?

La falla rápida (fail—fast) es un mecanismo en las colecciones de Java. Al atravesar un objeto de colección con un iterador, si el contenido del objeto de colección se modifica (agregado, eliminado, modificado) durante el proceso de recorrido, arrojará una excepción de modificación concurrente. excepción.

¿Cuál es el principio de fallo rápido?

El iterador accede directamente al contenido de la colección durante el recorrido y utiliza una variable modCount durante el recorrido.

Si el contenido de la colección cambia durante el recorrido, el valor de modCount cambiará .

Siempre que el iterador use hashNext() / next() antes de recorrer el siguiente elemento, verificará si la variable modCount es el valor modCount esperado y, de ser así, volverá al recorrido; de lo contrario, se lanzará una excepción y el recorrido finalizará. .

Sugerencia : la condición de lanzamiento de excepción aquí es detectar la condición de que modCount != ExpectedModCount. Si el valor de modCount se modifica cuando cambia la colección y se establece en el valor de modCount esperado, no se generará una excepción.

Por lo tanto, no puede programar operaciones concurrentes dependiendo de si se lanza o no esta excepción.Esta excepción solo se recomienda para detectar errores de modificación concurrente.

Cuéntame sobre la escena del fracaso rápido.

Todas las clases de colección del paquete java.util se basan en el mecanismo de falla rápida y no se pueden modificar al mismo tiempo en subprocesos múltiples (modificados durante el proceso de iteración), que se considera un mecanismo de seguridad.

Sugerencia : falla de seguridad (a prueba de fallas) También puede comprender que los contenedores en el paquete java.util.concurrent son fallas seguras y se pueden usar y modificar simultáneamente en subprocesos múltiples.

A continuación, hablemos de ConcurrentHashMap.

Hablemos de su estructura de datos y ¿por qué su concurrencia es tan alta?

La capa inferior de ConcurrentHashMap se basa en  数组 + 链表 la composición, pero la implementación específica es ligeramente diferente en jdk1.7 y 1.8. Permítanme hablar primero sobre su estructura de datos en 1.7:

Como se muestra en la figura anterior, ConcurrentHashMap se compone de una matriz de segmentos y una entrada Hash. Al igual que HashMap, sigue siendo una matriz más una lista vinculada

Segment es una clase interna de ConcurrentHashMap, los componentes principales son los siguientes:

static final class Segment<K,V> extends ReentrantLock implements Serializable {

    private static final long serialVersionUID = 2249069246763182397L;
    // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
    transient volatile HashEntry<K,V>[] table;
    transient int count; // 记得快速失败(fail—fast)么?
    transient int modCount; // 大小
    transient int threshold; // 负载因子
    final float loadFactor;
}

HashEntry es similar a HashMap, pero la diferencia es que HashEntry usa volatile para modificar su valor de datos y el siguiente nodo a continuación.

¿Cuáles son las características de los volátiles?

  • Asegura la visibilidad de diferentes subprocesos que operan en esta variable, es decir, un subproceso modifica el valor de una variable, y este nuevo valor es inmediatamente visible para otros subprocesos. (para visibilidad )

  • Está prohibido reordenar instrucciones. (lograr orden )

  • volatile solo puede garantizar la atomicidad para una sola lectura/escritura. i++ no garantiza atomicidad para tales operaciones .

No introduciré demasiado espacio, hablaré sobre el capítulo de subprocesos múltiples, todos saben que es seguro después de usarlo.

¿Cuéntame cuál es el motivo de su alta concurrencia?

En principio, ConcurrentHashMap usa tecnología de bloqueo de segmento , donde Segment hereda de ReentrantLock.

A diferencia de HashTable, tanto las operaciones de poner como las de obtener deben sincronizarse. En teoría, ConcurrentHashMap admite la concurrencia de subprocesos del número de matrices de segmento). Cada vez que un subproceso ocupa un bloqueo para acceder a un segmento, no afectará a otros segmentos. Es decir, si la capacidad es 16, su concurrencia es 16, lo que permite que 16 subprocesos operen 16 segmentos al mismo tiempo y es seguro para subprocesos.

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();//这就是为啥他不可以put null值的原因
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          
         (segments, (j << SSHIFT) + SBASE)) == null) 
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

Primero localiza el segmento y luego realiza la operación de colocación.

Echemos un vistazo a su código fuente de colocación, y sabrá cómo logra la seguridad de subprocesos. He comentado las frases clave.

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
     // 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
     HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
     V oldValue;
     try {
          HashEntry<K,V>[] tab = table;
          int index = (tab.length - 1) & hash;
          HashEntry<K,V> first = entryAt(tab, index);
          for (HashEntry<K,V> e = first;;) {
               if (e != null) {
                   K k;
                   // 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
                   if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
                        oldValue = e.value;
                        if (!onlyIfAbsent) {
                             e.value = value;
                             ++modCount;
                        }
                        break;
                   }
                   e = e.next;
                } else {
                     // 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
                     if (node != null)
                         node.setNext(first);
                     else
                         node = new HashEntry<K,V>(hash, key, value, first);
                         int c = count + 1;
                         if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                         else
                            setEntryAt(tab, index, node);
                         ++modCount;
                         count = c;
                         oldValue = null;
                         break;
                 }
            }
       } finally {
            //释放锁
            unlock();
       }
       return oldValue;
}

En primer lugar, en el primer paso, intentará adquirir el bloqueo, si la adquisición falla, debe haber competencia entre otros subprocesos y luego usar  scanAndLockForPut() el giro para adquirir el bloqueo.

  1. Intenta girar para adquirir un candado.
  2. Si se alcanza el número de reintentos  MAX_SCAN_RETRIES , se cambiará para bloquear la adquisición de bloqueo para garantizar que la adquisición pueda tener éxito.

¿Qué pasa con la lógica de su get?

La lógica de obtención es relativamente simple. Solo necesita ubicar la clave de un segmento específico a través del hash y luego ubicar el elemento específico a través del hash una vez. Dado que el atributo de valor en HashEntry se modifica con la palabra clave volátil, la visibilidad de la memoria está garantizada, por lo que es el último valor cada vez que se adquiere.

El método get de ConcurrentHashMap es muy eficiente, porque todo el proceso no requiere bloqueo .

¿Ha encontrado que aunque 1.7 puede admitir el acceso simultáneo a cada segmento, todavía hay algunos problemas?

Sí, porque es básicamente la forma de agregar un arreglo a una lista enlazada. Cuando vamos a la consulta, tenemos que recorrer la lista enlazada, lo que conducirá a una baja eficiencia. Este es el mismo problema que el HashMap de jdk1.7. , por lo que está completamente en jdk1.8.mejorado.

¿Cómo se ve su estructura de datos en jdk1.8?

Entre ellos, se abandona el bloqueo de segmento de segmento original y se adopta  CAS + synchronized para garantizar la seguridad de la concurrencia.

Es muy similar a HashMap. También cambió el HashEntry anterior a Node, pero la función sigue siendo la misma. El valor y el siguiente se modifican con volatile para garantizar la visibilidad, y también se introduce un árbol rojo-negro. Cuando la lista enlazada es mayores que cierto valor se convertirán (el valor predeterminado es 8).

¿Qué pasa con otras operaciones de acceso al valor? ¿Y cómo garantizar la seguridad del hilo?

La operación de colocación de ConcurrentHashMap sigue siendo relativamente complicada, y se puede dividir aproximadamente en los siguientes pasos:

  1. Calcule el código hash en función de la clave.
  2. Determine si se requiere inicialización.
  3. Es el Nodo ubicado por la clave actual. Si está vacío, significa que la ubicación actual puede escribir datos. Use CAS para intentar escribir. Si falla, el giro garantiza el éxito.
  4. Si está en la ubicación actual hashcode == MOVED == -1, debe expandirse.
  5. Si no está satisfecho, use el bloqueo sincronizado para escribir datos.
  6. Si el número es mayor que TREEIFY_THRESHOLDentonces se convertirá en un árbol rojo-negro.

Mencionaste anteriormente ¿qué es CAS? ¿Qué es el giro? 

CAS es una implementación de bloqueo optimista y un bloqueo ligero.

El flujo de la operación CAS se muestra en la siguiente figura y el subproceso no se bloquea al leer datos. Cuando se prepare para reescribir los datos, compare si el valor original ha sido modificado. Si no ha sido modificado por otros subprocesos, se reescribirá. Si se ha modificado, el proceso de lectura se volverá a ejecutar.

¿Puede CAS garantizar que los datos no hayan sido modificados por otros subprocesos?

No, por ejemplo, el clásico problema ABA, CAS no puede juzgar.

¿Qué es ABA? 

Es decir, llegó un subproceso y cambió el valor a B, y otro subproceso volvió a cambiar el valor a A. Para el subproceso juzgado en este momento, se encontró que su valor seguía siendo A, por lo que no No se cual fue el valor. No ha sido modificado por otros. De hecho, si solo persigues el resultado final correcto en muchas escenas, no importa.

Pero en el proceso real, aún es necesario registrar el proceso de modificación, como la modificación de fondos, debe tener un registro cada vez que lo modifique, para que pueda rastrearse.

Entonces, ¿cómo resolver el problema de ABA?

Solo use el número de versión para garantizarlo. Por ejemplo, cuando consulto su valor original antes de modificarlo, traeré un número de versión. Cada vez que emita un juicio, juzgaré el valor y el número de versión juntos. Si el juicio tiene éxito, añadiré el número de versión. 1.

El rendimiento de CAS es muy alto, pero sé que el rendimiento de sincronizado no es bueno. ¿Por qué hay más sincronizado después de la actualización de jdk1.8?

Synchronized siempre ha sido un bloqueo de peso pesado antes, pero luego los funcionarios de Java lo actualizaron, y ahora usa el método de actualización de bloqueo para hacerlo.

Para la forma sincronizada de adquirir bloqueos, JVM utiliza un método de optimización de actualización de bloqueo, que consiste en utilizar un bloqueo sesgado para dar prioridad al mismo subproceso y luego adquirir el bloqueo nuevamente. Si falla, se actualizará a un CAS ligero. lock Si falla, girará durante un breve período de tiempo para evitar que el sistema suspenda el hilo. Finalmente, si todo lo anterior falla, actualice a un candado pesado . Por lo tanto, se actualizó paso a paso y, al principio, se bloqueó de muchas maneras livianas.

¿Qué pasa con la operación de obtención de ConcurrentHashMap?

  • De acuerdo con la dirección del código hash calculado, si está en el depósito, devuelva el valor directamente.
  • Si es un árbol rojo-negro, obtenga el valor de acuerdo con el árbol.
  • Si no está satisfecho, recorra para obtener el valor de acuerdo con la lista vinculada.

 Resumen: 1.8 ha hecho un gran cambio en la estructura de datos de 1.7. Después de usar el árbol rojo-negro, se puede garantizar la eficiencia de la consulta ( ), e incluso canceló el ReentrantLock y lo cambió a sincronizado. Se puede ver que el O(logn)sincronizado la optimización está en su lugar en la nueva versión de JDK de.

Supongo que te gusta

Origin blog.csdn.net/m0_49508485/article/details/127734606
Recomendado
Clasificación