ConcurrentHashMap del código fuente de JAVA

Este artículo ha participado en el evento "Ceremonia de creación de recién llegados" para comenzar juntos el camino de la creación de oro.

Al igual que HashMap, la estructura de datos de ConcurrentHashMap en la versión 1.7 y 1.8 también es diferente.

La diferencia entre 1.7 y 1.8

1.7

ConcurrentHashMap en JDK1.7 se compone de una estructura de matriz de segmentos y una estructura de matriz de HashEntry, es decir, ConcurrentHashMap divide la matriz de cubos hash en matrices pequeñas (segmentos), y cada matriz pequeña se compone de n HashEntry.

Como se muestra en la figura a continuación, primero divida los datos en secciones de almacenamiento y luego asigne un bloqueo a cada sección de datos. Cuando un subproceso ocupa el bloqueo para acceder a una sección de datos, otras secciones de datos también pueden ser accedidas por otros subprocesos, dándose cuenta de lo real del acceso concurrente.

imagen-20220314141742909-16472386654097.pngSegment hereda ReentrantLock, por lo que Segment es una especie de bloqueo reentrante y desempeña el papel de bloqueo. El segmento tiene como valor predeterminado 16, es decir, la simultaneidad es 16.

1.8

En términos de estructura de datos, ConcurrentHashMap en JDK1.8 selecciona la misma matriz de nodos + lista vinculada + estructura de árbol rojo-negro que HashMap; en términos de implementación de bloqueo, se abandona el bloqueo de segmento original y se utiliza CAS + sincronizado para lograr más implementación detallada Bloqueo granular.

El nivel de bloqueo se controla a un nivel de elemento de matriz de cubo hash más detallado, es decir, solo el nodo principal de la lista vinculada (el nodo raíz del árbol rojo-negro ) debe bloquearse, y no lo hará. afectar a otros elementos de la matriz hash bucket Leer y escribir, mejorando en gran medida la concurrencia.

imagen-20220314142026203.png

¿Por qué usar el bloqueo incorporado sincronizado para reemplazar el bloqueo reentrante ReentrantLock en JDK1.8?

  1. En JDK1.6, se han introducido muchas optimizaciones para la implementación del bloqueo sincronizado, y sincronizado tiene múltiples estados de bloqueo, que se convertirán paso a paso desde sin bloqueo -> bloqueo parcial -> bloqueo ligero -> bloqueo pesado.
  2. Reduzca la sobrecarga de memoria. Suponiendo que se utilicen bloqueos de reentrada para la compatibilidad con la sincronización, cada nodo debe heredar AQS para la compatibilidad con la sincronización. Pero no todos los nodos necesitan soporte de sincronización, solo el nodo principal de la lista enlazada (el nodo raíz del árbol rojo-negro) necesita sincronizarse, lo que sin duda genera una gran pérdida de memoria.

Instrucciones básicas

definición constante

//最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
//默认容量
private static final int DEFAULT_CAPACITY = 16;
//最大的数组长度,toArray方法需要
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//负载因子
private static final float LOAD_FACTOR = 0.75f;
//链表树化的阈值
static final int TREEIFY_THRESHOLD = 8;
//红黑树变成链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//链表需要树化的最小容量要求
static final int MIN_TREEIFY_CAPACITY = 64;
//在进行扩容时单个线程处理的最小步长。
private static final int MIN_TRANSFER_STRIDE = 16;
//sizeCtl 中用于生成标记的位数。对于 32 位数组,必须至少为 6。
private static int RESIZE_STAMP_BITS = 16;
//可以帮助调整大小的最大线程数。必须适合 32 - RESIZE_STAMP_BITS 位。
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
//在 sizeCtl 中记录大小标记的位移。
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
//哈希表中的节点状态,会在节点的hash值中体现
static final int MOVED     = -1; // hash for forwarding nodes
static final int TREEBIN   = -2; // hash for roots of trees
static final int RESERVED  = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

//基本计数器值(拿来统计哈希表中元素个数的),主要在没有争用时使用,但也可作为表初始化竞赛期间的后备。通过 CAS 更新。
private transient volatile long baseCount;
//表初始化和调整大小控制。如果为负数,则表正在初始化或调整大小:-1 表示初始化,否则 -(1 + 活动调整大小线程的数量)。否则,当 table 为 null 时,保存要在创建时使用的初始表大小,或者默认为 0。初始化后,保存下一个元素计数值,根据该值调整表的大小。
private transient volatile int sizeCtl;
//调整大小时要拆分的下一个表索引(加一个)。
private transient volatile int transferIndex;
//调整大小和/或创建 CounterCell 时使用自旋锁(通过 CAS 锁定)。
private transient volatile int cellsBusy;
//计数单元表。当非空时,大小是 2 的幂。   与baseCount一起记录哈希表中的元素个数。
private transient volatile CounterCell[] counterCells;
复制代码

propagacion

/**
将散列的较高位传播(XOR)到较低位,并将最高位强制为 0。由于该表使用二次幂掩码,因此仅在当前掩码之上的位中变化的散列集总是会发生冲突。 (已知的例子是在小表中保存连续整数的 Float 键集。)因此,我们应用了一种变换,将高位的影响向下传播。在位扩展的速度、实用性和质量之间存在折衷。因为许多常见的散列集已经合理分布(所以不要从传播中受益),并且因为我们使用树来处理 bin 中的大量冲突,我们只是以最便宜的方式对一些移位的位进行异或,以减少系统损失,以及合并最高位的影响,否则由于表边界,这些最高位将永远不会用于索引计算。
*/
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
复制代码

Después de pasar por la propagación, el valor hash de todas las operaciones clave es un número mayor o igual a 0. Entonces ConcurrentHashMap usa el hash de Node para registrar el estado del nodo. Consulte las definiciones de constantes anteriores: MOVED, TREEBIN, RESERVED.

poner operación

image-20220314180212299-16472521346978.png

initTable

La operación de inicialización es muy simple, solo mire el código fuente directamente

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
    //while循环一直来检查table是不是已经被初始化好了
        while ((tab = table) == null || tab.length == 0) {
            //看变量是不是小于0,负数表示有其他线程正在则表正在初始化或调整大小,当前线程像调度器表示当前线程愿意让出CPU
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // 失去了初始化竞赛;只是自旋
            //CAS加锁:比较当前对象的SIZECTL偏移量的位置的值是不是sc,如果是则将SIZECTL偏移量的位置的值设置成-1。    如果当前线程成功设置成-1,那么其他线程再CAS的时候就会发现这个地方的值不是原来的sc了,就加锁失败,退出。
            //另外:sc>0表示哈希表初始化或要扩容的大小
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    //CAS成功后,再次判断table是不是未初始化,避免在CAS的之前一刻,其他线程完成了初始化操作。
                    if ((tab = table) == null || tab.length == 0) {
                        //计算扩容大小,如果没指定扩容大小,那么按默认容量初始化
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        // >>> 无符号右移,所有sc=(3/4)n,也就是n*0.75;所以这行代码的意思就是把下一次扩容的阈值设置给sc
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //最后将sc设置给sizeCtl,try成功的情况下,sizeCtl记录的则是下一次扩容的阈值;
                    sizeCtl = sc;
                }
                //退出初始化操作
                break;
            }
        }
        return tab;
    }
复制代码

añadirCuenta

La operación de transferencia ocurre cuando ocurre la expansión, y la expansión ocurre después de que aumenta el número de elementos:

Mire el diseño de conteo de ConcurrentHashmap antes de mirar el código fuente de addCount, para que sea más fácil de entender cuando mira el código fuente.

Descripción general del diseño de conteo

image-20220315144505540.png

lógica de conteo

image-20220315144203536.png

procedimiento fullAddCount

TODO por agregar

ayudaTransferir

TODO por agregar

La expansión y la asistencia en la expansión son asistidas al recibir tareas a través de múltiples subprocesos. La expansión consiste en apuntar primero a nextTable a una matriz de nodos de tamaño y longitud expandida, y luego usar varios subprocesos para ayudar a transferir los datos de la tabla a nextTable. La tarea de transferencia se recibe de la tabla en pasos por el método de recepción de la tarea.Después de que el último subproceso completa la tarea de transferencia, la tabla apunta a la tabla siguiente para completar la operación de expansión.

Supongo que te gusta

Origin juejin.im/post/7084931212871434271
Recomendado
Clasificación