9 imágenes para un análisis en profundidad de ConcurrentHashMap

17112206:

Prefacio

En el desarrollo diario, a menudo utilizamos HashMap de pares clave-valor, que se implementa mediante una tabla hash, intercambia espacio por tiempo y mejora el rendimiento de las consultas.

Pero en escenarios concurrentes de subprocesos múltiples, HashMap no es seguro para subprocesos.

Si desea utilizar la seguridad de subprocesos, puede utilizar ConcurrentHashMap, HashTable, Collections.synchronizedMap, etc.

Sin embargo, debido a que la granularidad del uso de sincronizado es demasiado grande para los dos últimos, generalmente no se usan, sino que se usa ConcurrentHashMap en el paquete concurrente.

En ConcurrentHashMap, se usa volátil para garantizar la visibilidad de la memoria, de modo que no es necesario "bloquear" para garantizar la atomicidad en escenarios de lectura.

Utilice CAS + sincronizado en escenarios de escritura. Sincronizado solo bloquea el primer nodo en una determinada posición de índice en la tabla hash, lo que equivale a un bloqueo detallado y aumenta el rendimiento de concurrencia.

Este artículo analizará el uso de ConcurrentHashMap, los principios de implementación de lectura, escritura y expansión, y las ideas de diseño.

Antes de leer este artículo, debe comprender las tablas hash, volátiles, CAS, sincronizadas, etc.

Para volátil, puede consultar este artículo: 5 casos y diagramas de flujo para ayudarlo a comprender la palabra clave volátil de 0 a 1

Para CAS y sincronizado, puede ver este artículo: 15,000 palabras, 6 casos de código y 5 diagramas esquemáticos para ayudarlo a comprender a fondo Sincronizado

Utilice ConcurrentHashMap

ConcurrentHashMap es un mapa seguro para subprocesos en escenarios concurrentes. Puede consultar y almacenar pares clave-valor K y V en escenarios concurrentes.

Los objetos inmutables son absolutamente seguros para subprocesos, independientemente de cómo los utilice el mundo exterior.

ConcurrentHashMap no es absolutamente seguro para subprocesos. Solo proporciona seguridad para subprocesos para métodos. Si se usa incorrectamente en la capa externa, aún causará inseguridad en subprocesos.

Veamos el siguiente caso. Use valor para almacenar el número de llamadas de incremento automático, inicie 10 subprocesos y ejecute cada uno de ellos 100 veces. El resultado final debería ser 1000 veces, pero el uso incorrecto da como resultado menos de 1000.


    public void test() {
//        Map<String, Integer> map = new HashMap(16);
        Map<String, Integer> map = new ConcurrentHashMap(16);

        String key = "key";
        CountDownLatch countDownLatch = new CountDownLatch(10);


        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    incr(map, key);
//                    incrCompute(map, key);
                }
                countDownLatch.countDown();
            }).start();
        }

        try {
            //阻塞到线程跑完
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //1000不到
        System.out.println(map.get(key));
    }

	private void incr(Map<String, Integer> map, String key) {
        map.put(key, map.getOrDefault(key, 0) + 1);
    }

En el método de incremento automático incr, primero se realiza la operación de lectura, luego se realiza el cálculo y finalmente se realiza la operación de escritura. Esta operación compuesta no garantiza la atomicidad, por lo que la acumulación final de todos los resultados no debe ser 1000.

La forma correcta de usarlo es usar el método predeterminado proporcionado por JDK8compute

El principio de implementación de ConcurrentHashMap computees utilizar medios de sincronización antes del cálculo.

	private void incrCompute(Map<String, Integer> map, String key) {
        map.compute(key, (k, v) -> Objects.isNull(v) ? 1 : v + 1);
    }

estructura de datos

Similar a HashMap, implementado usando tabla hash + lista vinculada/árbol rojo-negro

Tabla de picadillo

La implementación de la tabla hash se compone de matrices. Cuando ocurre un conflicto hash (el algoritmo hash obtiene el mismo índice), se utiliza el método de dirección en cadena para construir una lista vinculada.

imagen.png

Cuando los nodos en la lista vinculada son demasiado largos y la sobrecarga de recorrido y búsqueda es alta y excede el umbral (la lista vinculada tiene más de 8 nodos y la longitud de la tabla hash es mayor que 64), el árbol se transforma en un árbol rojo. árbol negro para reducir el recorrido y la sobrecarga de búsqueda, y la complejidad del tiempo se optimiza desde O (n) es (log n)

imagen.png

ConcurrentHashMap se compone de una matriz de nodos. Durante la expansión, habrá dos tablas hash, antiguas y nuevas: table y nextTable.

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {	
	//哈希表 node数组
	transient volatile Node<K,V>[] table;
    
    //扩容时为了兼容读写,会存在两个哈希表,这个是新哈希表
    private transient volatile Node<K,V>[] nextTable;
    
    // 默认为 0
    // 当初始化时, 为 -1
    // 当扩容时, 为 -(1 + 扩容线程数)
    // 当初始化或扩容完成后,为 下一次的扩容的阈值大小
    private transient volatile int sizeCtl;
    
    //扩容时 用于指定迁移区间的下标
    private transient volatile int transferIndex;
    
    //统计每个哈希槽中的元素数量
    private transient volatile CounterCell[] counterCells;
}

nodo

El nodo se utiliza para implementar los nodos de la matriz de la tabla hash y los nodos construidos en listas vinculadas cuando ocurre un conflicto hash.

//实现哈希表的节点,数组和链表时使用
static class Node<K,V> implements Map.Entry<K,V> {
    //节点哈希值
	final int hash;
	final K key;
	volatile V val;
    //作为链表时的 后续指针 
	volatile Node<K,V> next;    	
}

// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}

// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}

// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}

// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}

Valor hash del nodo

//转发节点
static final int MOVED     = -1;
//红黑树在数组中的节点
static final int TREEBIN   = -2;
//占位节点
static final int RESERVED  = -3;

Nodo de reenvío: herede el nodo y configúrelo en el primer nodo de un índice en la tabla hash anterior al expandir la capacidad. Cuando encuentre un nodo de reenvío, debe buscarlo en la nueva tabla hash.

static final class ForwardingNode<K,V> extends Node<K,V> {
    	//新哈希表
        final Node<K,V>[] nextTable;
    	
        ForwardingNode(Node<K,V>[] tab) {
            //哈希值设置为-1
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }
}

El nodo TreeBin del árbol rojo-negro en la matriz: hereda Node, primero apunta al primer nodo del árbol rojo-negro

static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;
    	//红黑树首节点
        volatile TreeNode<K,V> first;
}    

imagen.png

Nodo de árbol rojo-negro TreeNode

static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev; 
    	boolean red;
}

Nodo marcador de posición: heredar nodo. Cuando se necesita cálculo ( computermétodo de uso), primero use el nodo marcador de posición para ocupar el lugar. Después del cálculo, el nodo se construye para reemplazar el nodo marcador de posición.

	static final class ReservationNode<K,V> extends Node<K,V> {
        ReservationNode() {
            super(RESERVED, null, null, null);
        }

        Node<K,V> find(int h, Object k) {
            return null;
        }
    }

Principio de implementación

estructura

Durante la construcción, se verifican los parámetros de entrada, luego se calcula la capacidad de la tabla hash en función de la capacidad de datos y el factor de carga que se almacenarán y, finalmente, la capacidad de la tabla hash se ajusta a la potencia de 2.

No se inicializa durante la construcción, sino que espera hasta que se utiliza antes de crearlo (carga diferida)

	public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        //检查负载因子、初始容量
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        
        //concurrencyLevel:1
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        //计算大小 = 容量/负载因子 向上取整
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        //如果超过最大值就使用最大值 
        //tableSizeFor 将大小调整为2次幂
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        
        //设置容量
        this.sizeCtl = cap;
    }

leer-obtener

El escenario de lectura utiliza volátil para garantizar la visibilidad, incluso las modificaciones realizadas por otros subprocesos son visibles y no es necesario utilizar otros medios para garantizar la sincronización.

La operación de lectura necesita encontrar elementos en la tabla hash, codificar el valor hash mediante el algoritmo de perturbación y luego usar el valor hash para obtener el índice mediante el algoritmo hash, que se divide en múltiples situaciones según el primer nodo del índice. .

  1. El algoritmo de perturbación altera completamente el valor hash (para evitar causar demasiados conflictos hash) y el bit de signo &0 garantiza que el resultado sea positivo.

    int h = spread(key.hashCode())

    Algoritmo de perturbación: operación XOR de 16 bits de valores hash altos y bajos

    Después del algoritmo de perturbación, &HASH_BITS = 0x7fffffff (011111...), el bit de signo es 0 para garantizar que el resultado sea un número positivo

    Los valores hash negativos representan funciones especiales, como nodos de reenvío, primeros nodos de árboles, nodos de marcador de posición, etc.

    	static final int spread(int h) {
            return (h ^ (h >>> 16)) & HASH_BITS;
        }
    
  2. Utilice el valor hash codificado para obtener el índice (subíndice) en la matriz mediante el algoritmo hash

    n es la longitud de la tabla hash:(n = tab.length)

    (e = tabAt(tab, (n - 1) & h)

    h es el valor hash calculado y la posición del índice se puede encontrar mediante el valor hash % (longitud de la tabla hash - 1)

    Para mejorar el rendimiento, se estipula que la longitud de la tabla hash es la n potencia de 2. La longitud de la tabla hash en binario debe ser 1000..., y la longitud (n-1)en binario debe ser 0111...

    Por lo tanto, (n - 1) & hal calcular el índice, el resultado de la operación Y debe estar entre 0 ~ n - 1. Utilice operaciones de bits para mejorar el rendimiento.

  3. Después de obtener los nodos en la matriz, debes compararlos.

    Después de encontrar el primer nodo en la tabla hash, compare la clave para ver si es el nodo actual.

    Reglas de comparación: primero compare los valores hash. Si los valores hash del objeto son los mismos, puede ser el mismo objeto. También debe comparar la clave (== e igual). Si los valores hash son no es lo mismo, entonces definitivamente no es el mismo objeto.

    La ventaja de comparar primero los valores hash es mejorar el rendimiento de la búsqueda . Si se usa igual directamente, la complejidad del tiempo puede aumentar (como los iguales de String).

  4. Utilice el método de dirección en cadena para resolver conflictos hash, de modo que después de encontrar el nodo, pueda recorrer la lista o árbol vinculado; debido a la expansión de la tabla hash, es posible que también tenga que buscar en un nuevo nodo

    4.1 El primer nodo tiene relativamente éxito y regresa directamente

    4.2 El valor hash del primer nodo es negativo, lo que indica que el nodo es un caso especial: nodo de reenvío, primer nodo del árbol, nodo de marcador de posición de reserva calculado

    • Si es un nodo de reenvío y se está expandiendo, vaya a la nueva matriz para encontrarlo.
    • Si es TreeBin, busca en el árbol rojo-negro.
    • Si es un nodo de marcador de posición, devuelve vacío directamente.

    4.3 Recorrer la lista vinculada y comparar secuencialmente

obtener código

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //1.spread:扰动算法 + 让key的哈希值不能为负数,因为负数哈希值代表红黑树或ForwardingNode
    int h = spread(key.hashCode());
    //2.(n - 1) & h:下标、索引 实际上就是数组长度模哈希值 位运算效率更高
    //e:哈希表中对应索引位置上的节点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        //3.如果哈希值相等,说明可能找到,再比较key
        if ((eh = e.hash) == h) {
            //4.1 key相等说明找到 返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //4.2 首节点哈希值为负,说明该节点是转发节点,当前正在扩容则去新数组上找
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        
        //4.3 遍历该链表,能找到就返回值,不能返回null
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

escribir-poner

Al agregar elementos, utilice el método de sincronización CAS + sincronizado (solo bloquea un determinado primer nodo en la tabla hash) para garantizar la atomicidad.

  1. Obtenga el valor hash: algoritmo de perturbación + asegúrese de que el valor hash sea positivo
  2. La tabla hash está vacía, CAS garantiza una inicialización del hilo
	private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            //小于0 说明其他线程在初始化 让出CPU时间片 后续初始化完退出
            if ((sc = sizeCtl) < 0)
                Thread.yield(); 
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                //CAS将SIZECTL设置成-1 (表示有线程在初始化)成功后 初始化
                try {
                    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;
    }
  1. Pase el valor hash a través del algoritmo hash para obtener el nodo en el índicef = tabAt(tab, i = (n - 1) & hash)

  2. Manejar según diferentes situaciones.

    • 4.1 Cuando el primer nodo está vacío, CAS agrega directamente el nodo a la posición de índice.casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))

      imagen.png

    • 4.2 Cuando el hash del primer nodo es MOVER -1, significa que el nodo es un nodo de reenvío, lo que indica que se está expandiendo y ayuda a expandir la capacidad.

    • 4.3 Bloqueo del primer nodo

      • 4.3.1 Recorrer la lista vinculada para buscar y agregar/sobrescribir

        imagen.png

      • 4.3.2 Recorrer el árbol para buscar y agregar/sobrescribir

  3. addCountCuente los datos en cada nodo y verifique la expansión.

poner codigo

//onlyIfAbsent为true时,如果原来有k,v则这次不会覆盖
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //1.获取哈希值:扰动算法+确保哈希值为正数
    int hash = spread(key.hashCode());
    int binCount = 0;
    //乐观锁思想 CSA+失败重试
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //2.哈希表为空 CAS保证只有一个线程初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //3. 哈希算法求得索引找到索引上的首节点
        //4.1 节点为空时,直接CAS构建节点
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //4.2 索引首节点hash 为MOVED 说明该节点是转发节点,当前正在扩容,去帮助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //4.3 首节点 加锁
            synchronized (f) {
                //首节点没变
                if (tabAt(tab, i) == f) {
                    //首节点哈希值大于等于0 说明节点是链表上的节点  
                    //4.3.1 遍历链表寻找然后添加/覆盖
                    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;
                                //onlyIfAbsent为false允许覆盖(使用xxIfAbsent方法时,有值就不覆盖)
                                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;
                            }
                        }
                    }
                    //如果是红黑树首节点,则找到对应节点再覆盖
                    //4.3.2 遍历树寻找然后添加/覆盖
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        //如果是添加返回null,返回不是null则出来添加
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            //覆盖
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    //链表上的节点超过TREEIFY_THRESHOLD 8个(不算首节点) 并且 数组长度超过64才树化,否则扩容
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //5.添加计数,用于统计元素(添加节点的情况)
    addCount(1L, binCount);
    return null;
}

Expansión

Para evitar conflictos hash frecuentes, cuando el número de elementos en la tabla hash/la longitud de la tabla hash excede el factor de carga, expanda la capacidad (aumente la longitud de la tabla hash)

En términos generales, la expansión consiste en aumentar la longitud de la tabla hash en 2 veces. Por ejemplo, de 32 a 64, se garantiza que la longitud será una potencia de 2; si la longitud de expansión alcanza el límite superior del tipo entero, la Se utiliza el valor entero máximo.

Cuando se produce la expansión, la lista vinculada o el árbol en cada ranura de la matriz debe migrarse a la nueva matriz.

Si el procesador es multinúcleo, entonces esta operación de migración no la completa un solo subproceso, sino que otros subprocesos también ayudarán con la migración.

Durante la migración, deje que cada hilo migre varias ranuras de derecha a izquierda. Una vez completada la migración, se juzgará si se han completado todas las migraciones. De lo contrario, la migración continuará de forma circular.

La operación de expansión se realiza principalmente en transferel método, y la expansión se realiza principalmente en tres escenarios:

  1. addCount: Después de agregar el nodo, aumente el recuento y verifique la expansión.
  2. helpTransfer: Cuando se pone el hilo se comprueba que está migrando, para ayudar a ampliar la capacidad.
  3. tryPresize: Intente ajustar la capacidad (adición de lotes putAll, llamada cuando la longitud de la matriz del árbol no excede 64 treeifyBin)

Dividido en los siguientes 3 pasos.

  1. Calcule cuántas ranuras migrar cada vez según la cantidad de núcleos de CPU y la longitud total de la tabla hash, el mínimo es 16

  2. La nueva tabla hash está vacía, lo que indica que está inicializada.

  3. Migración circular

    • 3.1 Asigne el intervalo [bround, i] responsable de la migración (puede haber migración simultánea de varios subprocesos)

      imagen.png

    • 3.2 Migración: dividida en migración de lista vinculada y migración de árbol

      Migración de lista enlazada

      1. Hash completamente los nodos en la lista vinculada en el índice de la nueva tabla hash, índice + los dos subíndices de la longitud de la tabla hash anterior (similar a HashMap)

      2. Coloque el nodo en la lista vinculada de posición de índice (hash y longitud de la tabla hash), el resultado es 0 en la posición de índice de la nueva matriz, el resultado es 1 en la posición del índice de la nueva matriz + la longitud de la tabla hash anterior

        imagen.png

        Por ejemplo, la longitud de la tabla hash anterior es 16. En el índice 3, el valor binario de 16 es 10000, hash&16 => hash& 10000. Es decir, si el quinto bit del valor hash del nodo es 0, es colocado en la posición 3 de la nueva tabla hash. Si es 1, colóquelo en el subíndice 3 + 16 de la nueva tabla hash.

      3. Utilice el método de interpolación principal para construir una lista vinculada ln cuando el resultado del cálculo sea 0 y una lista vinculada hn cuando sea 1. Para facilitar la construcción de la lista vinculada, el nodo lastRun se encontrará primero: el nodo lastRun y los nodos posteriores son todos nodos en la misma lista vinculada, lo que facilita la migración.

        Primero construya lastRun antes de crear la lista vinculada, por ejemplo, lastRun e->f en la figura, primero coloque lastRun en la lista vinculada ln, luego recorra la lista vinculada original, recorra hasta a: a->e->f, atraviese a b: b->a ->e->f

      imagen.png

      1. Después de migrar cada posición de índice, el nodo de reenvío se establece en la posición correspondiente en la tabla hash original. Cuando otros subprocesos realizan operaciones de lectura y obtención, buscan en la nueva tabla hash de acuerdo con el nodo de reenvío y realizan operaciones de escritura y colocación en ayudar a ampliar la capacidad (otra migración de rango)

        imagen.png

Código de expansión

//tab 旧哈希表
//nextTab 新哈希表
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        //1.计算每次迁移多少个槽
        //n:哈希表长度(多少个槽)
        int n = tab.length, stride;
        //stride:每次负责迁移多少个槽
        //NCPU: CPU核数
        //如果是多核,每次迁移槽数 = 总槽数无符号右移3位(n/8)再除CPU核数  
        //每次最小迁移槽数 = MIN_TRANSFER_STRIDE = 16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
    
        //2.如果新哈希表为空,说明是初始化
        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用于记录 每次负责迁移的槽右区间下标,从右往左分配,起始为最右
            transferIndex = n;
        }
        //新哈希表长度
        int nextn = nextTab.length;
        //创建转发节点,转发节点一般设置在旧哈希表首节点,通过转发节点可以找到新哈希表
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        //advance:是否继续循环迁移
        boolean advance = true;
        // 
        boolean finishing = false; // to ensure sweep before committing nextTab
        //3.循环迁移
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            //3.1 分配负责迁移的区间
            //bound为左区间 i为右区间
            while (advance) {
                int nextIndex, nextBound;
                //处理完一个槽 右区间 自减
                if (--i >= bound || finishing)
                    advance = false;
                //transferIndex<=0说明 要迁移的区间全分配完
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                //CAS设置本次迁移的区间,防止多线程分到相同区间
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            
            //3.2 迁移
            
            //3.2.1 如果右区间i不再范围,说明迁移完
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                //如果完成迁移,设置哈希表、数量
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //CAS 将sizeCtl数量-1 表示 一个线程迁移完成 
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //如果不是最后一条线程直接返回
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    //是最后一条线程设置finishing为true  后面再循环 去设置哈希表、数量等操作
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            //3.2.2 如果旧哈希表i位置节点为空就CAS设置成转发节点
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            //3.2.3 如果旧哈希表该位置首节点是转发节点,说明其他线程已处理,重新循环
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                //3.2.4 对首节点加锁 迁移
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        //3.2.4.1 链表迁移
                        //首节点哈希值大于等于0 说明 是链表节点
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            //寻找lastRun节点 
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            //如果最后一次计算值是0
                            //lastRun节点以及后续节点计算值都是0构建成ln链表 否则 都是1构建成hn链表
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            
                            //遍历构建ln、hn链表 (头插)
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                //头插:Node构造第四个参数是后继节点
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            //设置ln链表到i位置
                            setTabAt(nextTab, i, ln);
                            //设置hn链表到i+n位置
                            setTabAt(nextTab, i + n, hn);
                            //设置转发节点
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        //3.2.4.2 树迁移
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

El principio de implementación no describe mucho sobre los árboles rojo-negros. Por un lado, hay demasiados conceptos de árboles rojo-negros y, por otro lado, casi los he olvidado (ya soy viejo y no puedo). No escribir árboles rojo-negros a mano como en la universidad)

Otro aspecto es: creo que es suficiente conocer los beneficios de usar árboles rojo-negro, y no se usa comúnmente en el trabajo, incluso si el árbol rojo-negro cambia de color, gira hacia la izquierda o hacia la derecha para cumplir con las condiciones del rojo. -Árbol negro, no tiene sentido. Los estudiantes interesados ​​pueden simplemente ir a estudiar.

Iterador

Los iteradores en ConcurrentHashMap son débilmente consistentes y usan la tabla hash registrada para reconstruir nuevos objetos al recuperarlos.

Iterador de entrada:

public Iterator<Map.Entry<K,V>> iterator() {
    ConcurrentHashMap<K,V> m = map;
    Node<K,V>[] t;
    int f = (t = m.table) == null ? 0 : t.length;
    return new EntryIterator<K,V>(t, f, 0, f, m);
}

iterador clave

public Enumeration<K> keys() {
    Node<K,V>[] t;
    int f = (t = table) == null ? 0 : t.length;
    return new KeyIterator<K,V>(t, f, 0, f, this);
}

iterador de valor

public Enumeration<V> elements() {
    Node<K,V>[] t;
    int f = (t = table) == null ? 0 : t.length;
    return new ValueIterator<K,V>(t, f, 0, f, this);
}

Resumir

ConcurrentHashMap utiliza la estructura de datos de una tabla hash. Cuando ocurre un conflicto hash, utiliza el método de dirección en cadena para resolverlo. Los nodos agrupados en el mismo índice se construyen en una lista vinculada. Cuando la cantidad de datos alcanza un cierto umbral, la lista enlazada se convierte en un árbol rojo-negro.

ConcurrentHashMap utiliza modificación volátil para almacenar datos, haciendo visibles las modificaciones a otros subprocesos en el escenario de lectura. No es necesario utilizar un mecanismo de sincronización. CAS y sincronizado se utilizan para garantizar la atomicidad en el escenario de escritura.

Al consultar datos con get, primero pase el valor hash de la clave a través del algoritmo de perturbación (XOR alto y bajo de 16 bits) y asegúrese de que el resultado sea un número positivo (con el bit de signo superior 0), y luego combínelo con la longitud de la tabla hash superior es -1 para encontrar el valor del índice. Después de encontrar el índice, busque de acuerdo con diferentes situaciones (la comparación determina primero el valor hash y luego determina la clave si es igual)

Al agregar / sobrescribir datos ingresados, primero encuentre la posición del índice a través del algoritmo de perturbación y hash, y luego busque de acuerdo con diferentes situaciones. Si se encuentra, se sobrescribirá y, si no se encuentra, se reemplazará.

Cuando se necesita expansión, el intervalo de ranura que necesita migrarse se organizará para el subproceso. Cuando otros subprocesos realicen la transferencia, también ayudarán con la migración. Cada vez que un subproceso migre una ranura, el nodo de reenvío se establecerá en el original. tabla hash, para que haya consultas de subprocesos. Puede buscar la nueva tabla hash a través del nodo de reenvío. Cuando se migran todas las ranuras, deje un hilo para configurar la tabla hash, la cantidad, etc.

El iterador utiliza una consistencia débil y se construye un nuevo objeto a través de una tabla hash al obtener el iterador.

ConcurrentHashMap solo garantiza una seguridad relativa de los subprocesos, pero no puede garantizar una seguridad absoluta de los subprocesos. Si necesita realizar una serie de operaciones, debe utilizarlo correctamente.

¡Este artículo es publicado por OpenWrite, un blog que publica varios artículos !

Lei Jun: La versión oficial del nuevo sistema operativo de Xiaomi, ThePaper OS, ha sido empaquetada. Una ventana emergente en la página de lotería de la aplicación Gome insulta a su fundador. El gobierno de Estados Unidos restringe la exportación de la GPU NVIDIA H800 a China. La interfaz de Xiaomi ThePaper OS está expuesto. Un maestro usó Scratch para frotar el simulador RISC-V y se ejecutó con éxito. Kernel de Linux Escritorio remoto RustDesk 1.2.3 lanzado, soporte mejorado para Wayland Después de desconectar el receptor USB de Logitech, el kernel de Linux falló Revisión aguda de DHH de "herramientas de empaquetado ": no es necesario construir la interfaz en absoluto (Sin compilación) JetBrains lanza Writerside para crear documentación técnica Herramientas para Node.js 21 lanzadas oficialmente
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/6903207/blog/10115611
Recomendado
Clasificación