Entrevista y luego preguntó ConcurrentHashMap, la formación y la preparación se puso este artículo!

En primer lugar, el fondo:

HashMap Hilo de seguridad

Debido entorno multi-hilo, el uso de Hashmap poner acción provocará un bucle infinito, lo que resulta en la utilización de la CPU cercana al 100%, por lo que no puede utilizar un HashMap en concurrencia.

Ineficiente recipiente HashTable

recipiente HashTable utilizando sincronizado para garantizar la seguridad hilo, pero HashTable en la feroz competencia en el hilo muy ineficiente.

Porque cuando un hilo para acceder al método de sincronización HashTable, método de sincronización HashTable acceso otros hilos puede estar bloqueado o entrar en el estado de interrogación. Tales como hilo de 1 objeto de un uso para agregar elementos, rosca 2 no será capaz de añadir elementos, y también no se puede usar el método get para obtener los elementos utilizando el método put, por lo que el disminuir la eficiencia de una competencia más intensa.

Segmentación de bloqueo

contenedor HashTable mostró razones de la baja eficiencia en un entorno altamente competitivo concurrencia, porque todas las discusiones acceso HashTable deben competir por la misma cerradura

Si el recipiente que la forma en cerraduras, cada cerradura para bloquear el contenedor en el que parte de los datos, así que cuando múltiples hilos acceder a diferentes segmentos de contenedores de datos, no existiría entre hilos contención de bloqueo, que se puede mejorar efectivamente la concurrencia la eficacia del acceso

Esto es las técnicas segmento de bloqueo ConcurrentHashMap utilizados, los primeros segmentos de datos almacenando, para cada pieza de datos y con una cerradura, cuando un hilo que sostiene los datos de acceso de bloqueo en el que un segmento de tiempo, el otro segmento de datos puede ser también otros subprocesos tienen acceso.

Algunos métodos requieren secciones transversales, como el tamaño () y containsValue (), que pueden necesitar para bloquear toda la tabla y no sólo un segmento, que requiere con el fin de bloquear todos los segmentos después de la operación, sino también con el fin de cerraduras de liberación en todos los segmentos .

Aquí, "el fin" es muy importante, o es probable que un punto muerto en el interior ConcurrentHashMap, es el segmento final de la matriz y sus variables miembro realidad final, sin embargo, sólo la matriz no se declara como definitiva para garantizar que los miembros de la matriz es final, que es necesario para garantizar la aplicación. Esto asegura que no hay punto muerto, debido a que el fin de obtener el bloqueo fijos.

array Segmento ConcurrentHashMap es una matriz de estructuras y estructuras HashEntry. Segmento es un ReentrantLock bloqueo de reentrada, juegan un papel en la cerradura en ConcurrentHashMap, pares de valores clave HashEntry se utilizan para almacenar datos.

A ConcurrentHashMap contiene una matriz Segmento HashMap estructura de segmentos y similares, y una matriz de una estructura de lista, un segmento contiene una matriz HashEntry, cada elemento de una lista es estructura HashEntry, un guardián de cada segmento de matriz HashEntry elemento, cuando el HashEntry matriz de datos modificado, debe obtener primero el correspondiente bloqueo de segmento.

En segundo lugar, los escenarios de aplicación

Cuando hay una gran matriz como sea necesario cuando varios subprocesos cuota puede considerar la posibilidad de dar a los jerárquicos múltiples nodos, y evitar gran cerradura. Considere algunos de los módulos y se pueden colocar por el algoritmo de hash.

De hecho, más de un hilo, cuando la tabla de datos de diseño de transacción (transacción refleja también un mecanismo de sincronización sentido), se puede poner una mesa como una matriz de necesidades de sincronización, se puede considerar una tabla separada transacción si la manipulación de datos demasiado (por eso se quiere evitar una mesa grande), tales como los campos de datos de dos niveles sub-listas.

En tercer lugar, la interpretación del código fuente

Las principales clases de entidad (1.7 y anteriores) en ConcurrentHashMap es tres: ConcurrentHashMap (toda la tabla hash), segmento (barriles), HashEntry (nodo)

La relación correspondiente entre el anterior se puede ver en la figura.

/**
* The segments, each of which is a specialized hash table
*/
final Segment[] segments;

Inalterable (inmutable) y variable (Volatile)

ConcurrentHashMap por completo al mismo tiempo que permite múltiples operaciones de lectura, una operación de lectura no requiere de bloqueo. Si el uso de técnicas convencionales, tales como la aplicación HashMap, si se permite que añadir o elementos de eliminación en el medio de la cadena de hash, la operación de lectura no bloquea los datos inconsistentes resultante.

tecnología de implementación ConcurrentHashMap es asegurar HashEntry casi inmutable. HashEntry cada nodo representante de una cadena de hash, cuya estructura se muestra a continuación:

 static final class HashEntry<K,V> {
     final K key;
     final int hash;
     volatile V value;
     final HashEntry next;
 }

Podemos ver que, además, otros valores son el valor final no es definitiva, lo que significa que no se puede agregar o cadena cola hash a partir de los nodos intermedios o eliminar, ya que tiene que ser modificado siguiente valor de referencia, todos los nodos sólo pueden modificarse desde la cabeza comenzar. Para la operación de venta, siempre se puede añadir a la cadena de la cabeza Hash.

Sin embargo, para la operación de eliminación, puede que tenga que eliminar un nodo de la media, que necesita ser eliminada frente a todos los nodos de toda la replicación de nuevo, eliminar los últimos puntos de nodo a nodo siguiente nodo. Esto también le explicará en detalle durante la operación de eliminación. Con el fin de garantizar que la operación de lectura puede ver el último valor, el valor se establece en volátiles, lo que evita el bloqueo.

otro

Con el fin de acelerar la velocidad y el posicionamiento de los segmentos de ranura segmento de hash, cada número de ranura de hash de segmento es 2 ^ n, de modo que la posición que puede ser posicionado mediante el cálculo del segmento de bit y la ranura de segmento de hash. Cuando el nivel de concurrencia valor por defecto 16, es decir, el número de segmentos, los 4 bits superiores de los valores de hash se determinan en el que la asignación de segmento.

Pero no nos olvidemos de la "Introducción a los algoritmos" La lección: el número de ranuras de hash no debe ser 2 ^ n, lo que puede dar lugar a una distribución desigual de depósito de hachís, que requiere una re-hash del valor hash de nuevo. (Esto parece un poco redundante)

Operación de posicionamiento:

final Segment segmentFor(int hash) {
     return segments[(hash >>> segmentShift) & segmentMask];
 }

Desde cerraduras segmentos de uso ConcurrentHashMap segmento para proteger los datos en los diferentes segmentos, entonces el tiempo de inserción y obtener los elementos, primero debe localizar el segmento de hash. Podemos ver ConcurrentHashMap utilizó por primera variante algoritmo de Wang / Jenkins picadillo de elementos hashCode una vez más hash.

Re-hash, su propósito es reducir la colisión de hash, de modo que los elementos se pueden distribuir uniformemente sobre diferentes Segmento, mejorando así la eficiencia del vaso de acceso. Si el hash de la mala calidad extrema, por lo que todos los elementos están en un segmento, no sólo los elementos de acceso lentos, bloqueo segmentada serán sin sentido. Hice una prueba, no realice directamente el cálculo de hash por re-hash.

System.out.println(Integer.parseInt("0001111", 2) & 15);
System.out.println(Integer.parseInt("0011111", 2) & 15);
System.out.println(Integer.parseInt("0111111", 2) & 15);
System.out.println(Integer.parseInt("1111111", 2) & 15);

Después de calcular el valor hash de la salida de los 15, por este ejemplo se puede encontrar si no re-hash, la colisión de hash será muy grave, porque mientras bajo como, no importa lo que el número es alto, es siempre el mismo valor hash. A continuación, realiza lo anterior los datos binarios resultado después de re-hash de la siguiente manera, para facilitar la lectura, los 32 bits superiores representan menos del 0, la barra vertical se divide cada cuatro.

0100|0111|0110|0111|1101|1010|0100|1110
1111|0111|0100|0011|0000|0001|1011|1000
0111|0111|0110|1001|0100|0110|0011|1110
1000|0011|0000|0000|1100|1000|0001|1010

Puede encontrar son ordenadas abrió cada bit de datos, y cada uno en este refrito puede participar para hacer hash digital de ellos, reduciendo así la colisión de hash. ConcurrentHashMap por segmento de posicionamiento hashing.

predeterminado SegmentShift es 28, segmentMask 15, entonces el número máximo de datos de hash binario es de 32 bits, no hay símbolos se mueven a la derecha 28, superior 4 bits de sí significa que participan cálculo de hash, (almohadilla >>> segmentShift ) y resultado del cálculo segmentMask 4,15,7 y 8, respectivamente, se puede ver que el valor hash no entre en conflicto.

final Segment segmentFor(int hash) {
    return segments[(hash >>> segmentShift) & segmentMask];
}

Estructura de datos

Todos los miembros son final, donde segmentMask y segmentShift principalmente para la sección de posicionamiento, véase más arriba método segmentFor.

Acerca de las estructuras de datos subyacentes Las tablas hash aquí no quieren hacer demasiado discusión. Un aspecto muy importante de la tabla hash Hash es la forma de resolver el conflicto, ConcurrentHashMap y HashMap misma manera, todo el mismo valor de dispersión del nodo en una cadena de hash. La diferencia es que el HashMap, utilizando una pluralidad de ConcurrentHashMap Hash sub-tablas, el segmento es decir (segmento).

Cada tabla de segmento corresponde a una sub-Hash, sus miembros de datos son los siguientes:

static final class Segment<K,V> extends ReentrantLock implements Serializable {
         /**
          * The number of elements in this segment's region.
          */
         transient volatileint count;
         /**
          * Number of updates that alter the size of the table. This is
          * used during bulk-read methods to make sure they see a
          * consistent snapshot: If modCounts change during a traversal
          * of segments computing size or checking containsValue, then
          * we might have an inconsistent view of state so (usually)
          * must retry.
          */
         transient int modCount;
         /**
          * The table is rehashed when its size exceeds this threshold.
          * (The value of this field is always (int)(capacity *
          * loadFactor).)
          */
         transient int threshold;
         /**
          * The per-segment table.
          */
         transient volatile HashEntry[] table;
         /**
          * The load factor for the hash table.  Even though this value
          * is same for all segments, it is replicated to avoid needing
          * links to outer object.
          * @serial
          */
         final float loadFactor;
 }

Contar para contar el número de segmento de datos, que es volátil, que se utiliza para coordinar la lectura y modificar las operaciones para garantizar que la operación de lectura se puede leer casi los últimos cambios.

De manera coordinada es tal que cada operación de cambio hizo cambios estructurales, tales como la adición de nodos de borrado (valor no cambia el nodo modificaciones estructurales) /, debe escribir el valor de conteo, cada operación de lectura se inicia para ser leído tomar el valor de recuento. Esto toma ventaja de las mejoras realizadas a Java 5 semántica de volátiles, no sucede antes de la relación entre la escritura y lectura de la misma variable volátil.

número modCount de estadísticas para cambiar la estructura del segmento, principalmente para detectar si múltiples segmentos cambian un proceso de recorrido de segmento, también hablará en detalle cuando las operaciones abarcan.

threashold valor límite se utiliza para indicar la necesidad de una repetición.

tabla de segmentos nodo de almacenamiento matriz, cada elemento de la matriz es una cadena de hash, representada por HashEntry. tabla es volátil, lo que hace posible la lectura de los últimos valores de la tabla sin sincronización. loadFactor representa el factor de carga.

remove Eliminar (llave)

public V remove(Object key) {
   hash = hash(key.hashCode());
   return segmentFor(hash).remove(key, hash, null);
}

Toda la operación es localizar el segmento, y a continuación, quitar confiada a la sección de operación. Cuando una pluralidad de operaciones de borrado al mismo tiempo, siempre y cuando se encuentran segmentos no son los mismos, pueden ser realizadas simultáneamente.

Este es el método de eliminación de segmentos para lograr:

V remove(Object key, int hash, Object value) {
     lock();
     try {
         int c = count - 1;
         HashEntry[] tab = table;
         int index = hash & (tab.length - 1);
         HashEntry first = tab[index];
         HashEntry e = first;
         while (e != null && (e.hash != hash || !key.equals(e.key)))
             e = e.next;
         V oldValue = null;
         if (e != null) {
             V v = e.value;
             if (value == null || value.equals(v)) {
                 oldValue = v;

                 // All entries following removed node can stay
                 // in list, but all preceding ones need to be
                 // cloned.
                 ++modCount;
                 HashEntry newFirst = e.next;
                 *for (HashEntry p = first; p != e; p = p.next)
                     *newFirst = new HashEntry(p.key, p.hash,
                                                   newFirst, p.value);
                 tab[index] = newFirst;
                 count = c; // write-volatile
             }
         }
         return oldValue;
     } finally {
         unlock();
     }
 }

Toda la operación se realiza en el caso de bloqueos de segmento Holdings, líneas en blanco antes de la línea se dirige principalmente a eliminar nodo e. A continuación, el nodo si no hay un retorno directo nula, de lo contrario, es necesario copiar de nuevo delante del nodo E, el punto final a la siguiente nodo en nodo de correo. No se necesita E nodo detrás de replicación, que puedan ser reutilizados.

La media del bucle for es qué hacer con ella? ** (marcados con *) ** a partir del código, es que después de todo la clonación posicional de entrada y de nuevo al frente a luchar, pero es necesario? Cada elemento es necesario quitar un elemento antes de que el clon de nuevo?

Esto es en realidad la entrada de la invariancia de la decisión, cuidadosas definiciones de entrada de observación encontraron, además de valor, todos los demás atributos se utilizan para modificar la final, lo que significa que ya no se puede cambiar después del primer set siguiente dominio y reemplazado todo antes de la clonación de un nodo.

En cuanto a por qué la entrada se establece en la invariancia, que no requiere sincronización con la invariancia de acceso con el consiguiente ahorro relacionado con el tiempo

El siguiente es un esquema

El segundo gráfico es un poco de un problema, el nodo debe ser replicado en el valor del nodo 2 frente al valor de nodo de 1 en la parte posterior, que es exactamente el orden inverso del nodo original, pero afortunadamente, esto no afecta a nuestra discusión.

Eliminar toda la aplicación no es complicado, pero requiere prestar atención a los siguientes puntos.

  • En primer lugar, cuando hay un nodo que desea eliminar el valor para borrar el último paso recuento de menos uno. Este debe ser el último paso de la operación, o la operación de lectura puede no ver las modificaciones estructurales hechas antes de segmento.

  • En segundo lugar, retire comienza la ejecución asignará una tabla de variables locales Tab, debido a que la tabla de variables es volátil, de lectura y escritura de variables volátiles gran sobrecarga. El compilador no puede hacer ninguna lectura y escritura de variables volátiles optimización, acceso directo a múltiples variables de instancia no volátiles tuvo poco efecto, el compilador optimizar en consecuencia.

conseguir la operación

ConcurrentHashMap la operación de obtención sea confiada a los métodos mirada Segmento Direct método get Segmento:

V get(Object key, int hash) {
     if (count != 0) { // read-volatile 当前桶的数据个数是否为0
         HashEntry e = getFirst(hash);  得到头节点
         while (e != null) {
             if (e.hash == hash && key.equals(e.key)) {
                 V v = e.value;
                 if (v != null)
                     return v;
                 return readValueUnderLock(e); // recheck
             }
             e = e.next;
         }
     }
     returnnull;
 }

consiguen operaciones no requieren una cerradura.

A menos que se va a volver a leer cerradura vacío valor leído, sabemos método get HashTable es la necesidad de bloquear el contenedor, a continuación, obtener ConcurrentHashMap de operaciones es cómo hacerlo desbloqueado? La razón es su método get para ser utilizado en las variables globales se definen como volátiles

El primer paso es visitar la variable de recuento, que es una variable volátil, ya que toda la operación de modificación es en curso en los cambios estructurales va a escribir la última variable de recuento de pasos, a través de este mecanismo para garantizar que la operación puede ser casi obtener las últimas actualizaciones de construcción. Para la actualización no estructural, es decir, cambiando el valor del nodo, debido HashEntry el valor de la variable es volátil, pero también para asegurar de leer el último valor.

El siguiente paso se lleva a cabo de acuerdo con picadillo y la cadena de clave hash nodos hallazgo ergódicos a adquirir, si no lo encuentra, de nuevo acceso directo nulo. Las causas de las cadenas de hash no necesitan atravesar ese puntero enlace bloqueado siguiente es definitiva. Sin embargo, el puntero de cabeza no es definitiva, que se devuelve por el método (hash) GetFirst, el valor está presente en la tabla de matriz.

Esto hace GetFirst (hash) puede devolver nodo de cabecera anticuado, por ejemplo, cuando se realiza un método get, realice acaba de terminar GetFirst (hash) después de otro hilo para llevar a cabo la eliminación y actualizar el nodo de cabecera, lo que llevó obtener método el retorno del nodo principal no es hasta la fecha. Esto es permisible, mediante la coordinación de mecanismo para contar las variables, conseguir casi capaz de leer los datos más recientes, aunque puede no estar al día. Para obtener los últimos datos, sólo un completamente sincronizada.

Por último, si la petición del nodo se encuentra, se determina si un valor no nulo devuelto directamente, o en un estado donde un bloqueo de lectura. Puede parecer difícil de entender, el valor teórico del nodo no puede estar vacía, ya que cuando se ha puesto para determinar si hay que tirar NullPointerException está vacía. La única fuente es el valor predeterminado de valores nulos en HashEntry, porque HashEntry el valor no es lectura final, no síncrono es posible leer a un valor nulo.

Mira comunicado operación cuidadosamente puesto: tab[index] = new HashEntry(key, hash, first, value)En esta declaración, HashEntry constructor y asignación del valor de la tab[index]asignación pueden ser reordenadas, lo que puede hacer que el nodo está vacía.

Aquí, cuando V está vacía, puede ser un hilo está cambiando nodo, mientras que la operación de obtención anterior ninguno de la cerradura, según Bernstein condición, después de leer escritura o de lectura-escritura hará que los datos inconsistentes, así que aquí de nuevo en este correo cerradura leyó de nuevo, lo que garantiza el valor correcto.

V readValueUnderLock(HashEntry e) {
     lock();
     try {
         return e.value;
     } finally {
         unlock();
     }
 }

Las estadísticas de la cuenta corriente de campo para los valores de tamaño HashEntry segement de valor almacenado. Como variables volátiles se pueden realizar entre la visibilidad de las discusiones, se puede leer multihilo simultáneo, y garantiza que no lee los valores de expiración, pero sólo puede ser escribir un único subproceso (hay un caso que puede ser multiproceso a escribir, a escribir no depende del valor del valor original), no hay necesidad de entrar en operaciones de sólo lectura no necesitan escribir variables compartidas contar y valor, por lo que no se puede bloqueado.

La razón no ha leído el valor caducado, se basa en el principio suceder antes de java modelo de memoria, escribe en el campo volátil antes de leer, modificar e incluso si la adquisición de dos hilos simultáneamente las variables volátiles, consigo pueden obtener la última operación valor, que es la escena con una aplicación clásica de los volátiles reemplazar cerraduras

operación put

Del mismo modo la operación de venta está a cargo de poner la sección método. El párrafo siguiente es método put:

V put(K key, int hash, V value, boolean onlyIfAbsent) {
     lock();
     try {
         int c = count;
         if (c++ > threshold) // ensure capacity
             rehash();
         HashEntry[] tab = table;
         int index = hash & (tab.length - 1);
         HashEntry first = tab[index];
         HashEntry e = first;
         while (e != null && (e.hash != hash || !key.equals(e.key)))
             e = e.next;
         V oldValue;
         if (e != null) {
             oldValue = e.value;
             if (!onlyIfAbsent)
                 e.value = value;
         }
         else {
             oldValue = null;
             ++modCount;
             tab[index] = new HashEntry(key, hash, first, value);
             count = c; // write-volatile
         }
         return oldValue;
     } finally {
         unlock();
     }
 }

Este método es también el caso de sujeción de la cerradura sección (bloquear el segmento entero), que por supuesto es seguro para la ejecución concurrente, la modificación simultánea de datos no se lleva a cabo, han de tener un indicador para determinar si los estados para garantizar que la capacidad es insuficiente puede refrito.

A continuación hay que encontrar si existe una clave de nodo de la misma, si está presente, sustituir directamente el valor de este nodo. De lo contrario, crear un nuevo nodo y añadirlo a la cabeza de la cadena de hash, entonces tenemos que cambiar el valor de modCount y contar, también modificar el valor de recuento debe ser colocado en el último paso.

método llama al método put refrito, ReAsH implementado método también era muy compacto, la principal ventaja del tamaño de la tabla es de 2 ^ n, no se presenta aquí.

Y más difícil de entender es la frase int index = hash & (tab.length - 1), el segmento original que es la tabla hash real tabla hash es decir, cada segmento es en un sentido tradicional, como se muestra arriba, a partir de la diferencia estructural entre los dos se puede ver, esto es necesario para averiguar posición de entrada en la que la mesa, después de obtener la entrada es el primer nodo de la cadena, si e! = null, encontró la explicación, es necesario sustituir el valor del nodo (onlyIfAbsent == false), de lo contrario, necesitamos una nueva entrada, su sucesor es el primero, y dejar que la pestaña [índice] razón para ello, ¿qué significa? De hecho, esta nueva entrada se inserta en la cabeza de la cadena, el resto es muy fácil de entender

Dado que se necesitaba el método para poner la operación de escritura variable compartida, por lo que con el fin de hilo de seguridad, tiene que cerradura de variables de operación compartida. Segmento método PUT para localizar primero, y luego insertar en el segmento. Someterse operación de inserción requiere dos pasos, el primer paso para determinar si existe la necesidad de expansión HashEntry array segmento, el segundo paso se coloca entonces en la matriz de HashEntry posición del elemento aditivo.

  • ** necesidad de expansión. ** Antes de insertar el elemento en primer lugar determinar si el segmento en el array HashEntry excede la capacidad (umbral), y si se supera el umbral, la expansión de matriz. Vale la pena mencionar que el segmento juez de expansión es más apropiado que HashMap, HashMap se debe a que los elementos de inserción determinar si el elemento ha alcanzado su capacidad, si se tratara de llegar a la expansión, pero se introduce ningún elemento nuevo después de la expansión es probable, entonces HashMap sería una expansión inútil.

  • ** ¿Cómo expansión. ** cuando la primera expansión creará una matriz de dos veces la capacidad original, entonces los elementos de la matriz original, después de re-hash insertan en una matriz nueva. Con el fin de manera eficiente ConcurrentHashMap no para la expansión de todo el recipiente, pero sólo para un cierto segmento de expansión.

Otra operación es containsKey, esta aplicación será mucho más sencillo, ya que no es necesario leer el valor:

boolean containsKey(Object key, int hash) {
     if (count != 0) { // read-volatile
         HashEntry e = getFirst(hash);
         while (e != null) {
             if (e.hash == hash && key.equals(e.key))
                 returntrue;
             e = e.next;
         }
     }
     returnfalse;
 }

size () operación

Si queremos contar el tamaño de todo el ConcurrentHashMap de elementos, es necesario contar con todo el tamaño del segmento del elemento en la suma. Segmento en el conteo variable global es una variable volátil, en escenarios de multiproceso, no se cuenta directamente la suma de todo el segmento se puede obtener toda la ConcurrentHashMap tamaño de la misma?

No, aunque se puede obtener el último valor de la suma de cada cargo segmento, pero luego se acostumbró puede acumularse antes de que los cambios en el recuento, entonces no serán permitidas las estadísticas. Por lo tanto, el enfoque más seguro es el momento de poner todo el tamaño estadístico de la opción de venta del segmento, quitar y limpiar todos los métodos para bloquear, pero este enfoque es claramente muy ineficiente.

Debido a que la probabilidad de un cambio en el proceso de operación de conteo conteo acumulado, antes de la acumulación tenía una muy pequeña, por lo enfoque ConcurrentHashMap es primer intento de contar dos veces por no bloqueo tamaño Segmento Segmento cada forma, si el proceso estadístico, el contenedor recuento de los cambios, entonces bloqueado de nuevo utilizando el método estadístico para el tamaño de todos los segmentos.

Así ConcurrentHashMap es cómo determinar si el buque ha cambiado en las estadísticas cuando? variable de uso modCount, el elemento frontal modCount variable se añadió a 1, a continuación, comparar modCount si los cambios en el tamaño antes y después de las estadísticas ponen, retirar y método de limpieza en funcionamiento, por lo que el tamaño de la embarcación ha cambiado.

Publicado 50 artículos originales · ganado elogios 1711 · Vistas 2,24 millones +

Supongo que te gusta

Origin blog.csdn.net/zl1zl2zl3/article/details/105398733
Recomendado
Clasificación