Análisis de código fuente central de ConcurrentHashMap

Título de la imagen

Uno solo debe olvidarse de uno mismo y amar a los demás para que pueda estar tranquilo, feliz y noble. -Tolstoi, Anna Karenina

0 Prefacio

Thread-safe Map-ConcurrentHashMap, estudiemos cuál es la diferencia en comparación con HashMap, ¿por qué podemos garantizar la seguridad de los hilos?

1 sistema de herencia

Título de la imagen

Título de la imagen
Similar a HashMap, la estructura de las matrices y las listas vinculadas es casi la misma. Ambos implementan la interfaz Map y heredan la clase abstracta AbstractMap. La mayoría de los métodos también son iguales. ConcurrentHashMap contiene casi todos los métodos de HashMap.

2 propiedades

  • Array bin. La inicialización se retrasa solo después de la primera inserción. El tamaño es siempre una potencia de 2. Acceso directo por el iterador.
    Título de la imagen
  • La siguiente tabla a usar; no nula solo cuando se expande
    Título de la imagen
  • Valor de contador básico, utilizado principalmente cuando no hay contención, y también utilizado como retroalimentación durante la competencia de inicialización de la mesa.
    Título de la imagen
  • Si el control de la inicialización y expansión de la tabla es negativo, la tabla se inicializará o expandirá: -1 se usa para inicializar -N el número de subprocesos de expansión activos. De lo contrario, cuando la tabla sea nula, mantenga el tamaño de la tabla inicial que se usará al crear, o por defecto Es 0. Después de la inicialización, mantenga el valor de recuento de elementos de la siguiente tabla de expansión.
    Título de la imagen
  • Índice de la siguiente tabla que se dividirá durante la expansión (más 1)
    Título de la imagen
  • El bloqueo de expansión y / o giro se usa al crear CounterCell (bloqueado mediante CAS)
    Título de la imagen
  • Tabla de contraceldas. Si no es nulo, el tamaño es una potencia de 2.
    Título de la imagen
  • Nodo: una estructura de datos que contiene claves, valores y valores hash clave, donde tanto el valor como el siguiente se modifican con volátil para garantizar la visibilidad
    Título de la imagen
  • Un nodo de nodo especial, el valor hash del nodo de transferencia se MOVÍA, -1. Almacena la referencia de nextTable. El nodo se inserta en el encabezado del contenedor durante la transferencia. ForwardingNode solo jugará un papel como marcador de posición cuando la tabla se expanda. El símbolo se coloca en la tabla para indicar que el nodo actual es nulo o se ha movido,
    Título de la imagen

3 método de construcción

3.1 Sin parámetros

  • Cree un nuevo mapa vacío con el tamaño de tabla inicial predeterminado (16)
    Título de la imagen

3.2 Participación

  • Cree un nuevo mapa vacío cuyo tamaño de tabla inicial pueda acomodar el número especificado de elementos sin tener que ajustar dinámicamente el tamaño.
    Título de la imagen
    -Cree un nuevo mapa con la misma asignación que el mapa dado
    Título de la imagen

Tenga en cuenta que sizeCtl mantendrá temporalmente la capacidad de una potencia de dos valores.

Al crear instancias de ConcurrentHashMap con parámetros, el tamaño de la tabla se ajustará de acuerdo con los parámetros. Suponiendo que el parámetro sea 100, eventualmente se ajustará a 256 para garantizar que el tamaño de la tabla sea siempre una potencia de 2.

tableSizeFor

  • Para una capacidad requerida dada, devuelve el tamaño de la tabla en potencias de 2
    Título de la imagen

inicialización perezosa de la tabla

ConcurrentHashMap solo inicializa el valor sizeCtl en el constructor, y no inicializa directamente la tabla, sino que retrasa la inicialización de la primera tabla de operaciones de colocación. Pero put puede ejecutarse simultáneamente, ¿cómo asegurarse de que la tabla se inicialice solo una vez?

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    // 进入自旋
    while ((tab = table) == null || tab.length == 0) {
        // 若某线程发现sizeCtl<0,意味着其他线程正在初始化,当前线程让出CPU时间片
        if ((sc = sizeCtl) < 0) 
            Thread.yield(); // 失去初始化的竞争机会; 直接自旋
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                // 有可能执行至此时,table 已经非空,所以做双重检验
                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 = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
复制代码

El subproceso que realiza la primera operación de ejecución ejecutará el método Unsafe.compareAndSwapInt para modificar sizeCtl a -1, y solo un subproceso puede modificarse con éxito, mientras que otros subprocesos solo pueden dejar intervalos de tiempo de CPU a través de Thread.yield () para esperar a que se complete la inicialización de la tabla.

4 put

La tabla se ha inicializado y la operación de colocación usa CAS + sincronizado para implementar operaciones de inserción o actualización concurrentes.

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 计算hash
    int hash = spread(key.hashCode());
    int binCount = 0;
    // 自旋保证可以新增成功
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // step1. table 为 null或空时进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // step 2. 若当前数组索引无值,直接创建
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // CAS 在索引 i 处创建新的节点,当索引 i 为 null 时,即能创建成功,结束循环,否则继续自旋
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // step3. 若当前桶为转移节点,表明该桶的点正在扩容,一直等待扩容完成
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // step4. 当前索引位置有值
        else {
            V oldVal = null;
            // 锁定当前槽点,保证只会有一个线程能对槽点进行修改
            synchronized (f) {
                // 这里再次判断 i 位置数据有无被修改
                // binCount 被赋值,说明走到了修改表的过程
                if (tabAt(tab, i) == f) {
                    // 链表
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 值有的话,直接返回
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            // 将新增的元素赋值到链表的最后,退出自旋
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 红黑树,这里没有使用 TreeNode,使用的是 TreeBin,TreeNode 只是红黑树的一个节点
                    // TreeBin 持有红黑树的引用,并且会对其加锁,保证其操作的线程安全
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        // 满足if的话,把老的值给oldVal
                        // 在putTreeVal方法里面,在给红黑树重新着色旋转的时候
                        // 会锁住红黑树的根节点
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // binCount不为空,并且 oldVal 有值的情况,说明已新增成功
            if (binCount != 0) {
                // 链表是否需要转化成红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                // 槽点已经上锁,只有在红黑树或者链表新增失败的时候
                // 才会走到这里,这两者新增都是自旋的,几乎不会失败
                break;
            }
        }
    }
    // step5. check 容器是否需要扩容,如果需要去扩容,调用 transfer 方法扩容
    // 如果已经在扩容中了,check有无完成
    addCount(1L, binCount);
    return null;
}
复制代码

4.2 Proceso de ejecución

  1. Si la matriz está vacía, inicialice, después de completar, vaya a 2
  2. Calcular si el depósito actual tiene un valor
    • Ninguno, se crea CAS, continuará girando después de la falla, hasta que tenga éxito
    • Sí, pasa a 3
  3. Determine si el depósito es un nodo de transferencia (expansión de capacidad)
    • Sí, ha estado girando y esperando que se complete la expansión, y luego se agregó
    • No, pasa a 4
  4. El depósito tiene valor, agregue el bloqueo de sincronización al depósito actual
    • Lista vinculada, agregue nodos al final de la cadena
    • Árbol rojo y negro, nuevo método para la versión de árbol rojo y negro
  5. Después de completar la adición, verifique si se requiere expansión

¡La implementación de bloquear los tres ejes mediante sincronización spin + CAS + es muy inteligente y nos proporciona las mejores prácticas para diseñar código concurrente!

5 transferencia-expansión

Al final del método put para verificar si se requiere expansión, ingrese el método de transferencia desde el método addCount del método put.

Lo principal es crear una nueva matriz vacía y luego mover y copiar cada elemento a la nueva matriz.

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 旧数组的长度
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    // 如果新数组为空,初始化,大小为原数组的两倍,n << 1
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    // 新数组长度
    int nextn = nextTab.length;
    // 若原数组上是转移节点,说明该节点正在被扩容
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    // 自旋,i 值会从原数组的最大值递减到 0
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            // 结束循环的标志
            if (--i >= bound || finishing)
                advance = false;
            // 已经拷贝完成
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            // 每次减少 i 的值
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        // if 任意条件满足说明拷贝结束了
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            // 拷贝结束,直接赋值,因为每次拷贝完一个节点,都在原数组上放转移节点,所以拷贝完成的节点的数据一定不会再发生变化
            // 原数组发现是转移节点,是不会操作的,会一直等待转移节点消失之后在进行操作
            // 也就是说数组节点一旦被标记为转移节点,是不会再发生任何变动的,所以不会有任何线程安全的问题
            // 所以此处直接赋值,没有任何问题。
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            synchronized (f) {
                // 节点的拷贝
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        // 如果节点只有单个数据,直接拷贝,如果是链表,循环多次组成链表拷贝
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 在新数组位置上放置拷贝的值
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        // 在老数组位置上放上 ForwardingNode 节点
                        // put 时,发现是 ForwardingNode 节点,就不会再动这个节点的数据了
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    // 红黑树的拷贝
                    else if (f instanceof TreeBin) {
                        // 红黑树的拷贝工作,同 HashMap 的内容,代码忽略
                        ...
                        // 在老数组位置上放上 ForwardingNode 节点
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}
复制代码

Proceso de ejecución

  1. Primero copie todos los valores de la matriz original a la nueva matriz después de la expansión, primero copie desde el final de la matriz
  2. Al copiar las ranuras de una matriz, primero bloquee las ranuras de la matriz original. Cuando copie a una nueva matriz con éxito, asigne las ranuras de la matriz original al nodo de transferencia
  3. En este momento, si hay nuevos datos que deben colocarse en la ranura, se descubre que la ranura es un nodo de transferencia y siempre esperará, por lo que los datos correspondientes a la ranura no cambiarán hasta que se complete la expansión.
  4. Copie desde el final de la matriz a la cabeza. Cada vez que la copia se realiza correctamente, los nodos en la matriz original se configuran como nodos de transferencia hasta que todos los datos de la matriz se copien en la nueva matriz. Toda la matriz se asigna directamente al contenedor de la matriz y la copia se completa.

6 Resumen

ConcurrentHashMap, como un mapa concurrente, es un punto necesario para las entrevistas y un contenedor concurrente que debe dominarse en el trabajo.

Supongo que te gusta

Origin juejin.im/post/5e934e215188256bdf72b691
Recomendado
Clasificación