9 fotos para uma análise aprofundada do ConcurrentHashMap

17112206:

Prefácio

No desenvolvimento diário, costumamos usar HashMap de pares chave-valor, que é implementado usando uma tabela hash, trocando espaço por tempo e melhorando o desempenho da consulta.

Mas em cenários simultâneos multithread, o HashMap não é seguro para threads.

Se quiser usar segurança de thread, você pode usar ConcurrentHashMap, HashTable, Collections.synchronizedMap, etc.

No entanto, como a granularidade do uso de sincronizado é muito grande para os dois últimos, eles geralmente não são usados. Em vez disso, é usado ConcurrentHashMap no pacote simultâneo.

No ConcurrentHashMap, o volátil é usado para garantir a visibilidade da memória, para que não haja necessidade de "travar" para garantir a atomicidade nos cenários de leitura.

Use CAS + sincronizado em cenários de gravação. Sincronizado bloqueia apenas o primeiro nó em uma determinada posição de índice na tabela hash, o que equivale ao bloqueio refinado e aumenta o desempenho da simultaneidade.

Este artigo analisará o uso de ConcurrentHashMap, os princípios de implementação de leitura, escrita e expansão e ideias de design.

Antes de ler este artigo, você precisa entender tabelas hash, voláteis, CAS, sincronizadas, etc.

Para voláteis, você pode conferir este artigo: 5 casos e fluxogramas para ajudá-lo a entender a palavra-chave volátil de 0 a 1

Para CAS e sincronizado, você pode visualizar este artigo: 15.000 palavras, 6 casos de código e 5 diagramas esquemáticos para ajudá-lo a entender completamente o Sincronizado

Usar ConcurrentHashMap

ConcurrentHashMap é um mapa thread-safe em cenários simultâneos. Ele pode consultar e armazenar pares de valores-chave K e V em cenários simultâneos.

Objetos imutáveis ​​são absolutamente seguros para threads, independentemente de como são usados ​​pelo mundo exterior.

ConcurrentHashMap não é absolutamente seguro para threads. Ele apenas fornece segurança de thread para métodos. Se usado incorretamente na camada externa, ainda causará insegurança de thread.

Vejamos o seguinte caso. Use valor para armazenar o número de chamadas de incremento automático, inicie 10 threads e execute cada uma delas 100 vezes. O resultado final deve ser 1.000 vezes, mas o uso incorreto resulta em menos de 1.000.


    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);
    }

No método de incremento automático incr, a operação de leitura é realizada primeiro, depois o cálculo é realizado e, por fim, a operação de escrita.Esta operação composta não garante atomicidade, portanto o acúmulo final de todos os resultados não deve ser 1000.

A maneira correta de usá-lo é usar o método padrão fornecido pelo JDK8compute

O princípio da implementação do ConcurrentHashMap computeé usar meios de sincronização inseridos antes do cálculo.

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

estrutura de dados

Semelhante ao HashMap, implementado usando tabela hash + lista vinculada/árvore vermelho-preto

Tabela hash

A implementação da tabela hash é composta por arrays.Quando ocorre um conflito de hash (o algoritmo hash obtém o mesmo índice), o método de endereço de cadeia é usado para construir uma lista vinculada.

imagem.png

Quando os nós na lista vinculada são muito longos e a sobrecarga de travessia e pesquisa é alta e excede o limite (a lista vinculada tem mais de 8 nós e o comprimento da tabela hash é maior que 64), a árvore é transformada em um vermelho - árvore preta para reduzir a sobrecarga de travessia e pesquisa, e a complexidade do tempo é otimizada de O (n) é (log n)

imagem.png

ConcurrentHashMap é composto pelo array Node. Durante a expansão, haverá duas tabelas hash, antigas e novas: table e 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;
}

O nó é usado para implementar os nós da matriz da tabela hash e os nós construídos em listas vinculadas quando ocorre um conflito de 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 de hash do nó

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

Nó de encaminhamento: Herde o nó e configure-o no primeiro nó de um índice na tabela hash antiga ao expandir a capacidade. Ao encontrar um nó de encaminhamento, você deve procurá-lo na nova tabela 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;
        }
}

O nó TreeBin da árvore rubro-preta na matriz: herda o nó, primeiro aponta para o primeiro nó da árvore rubro-preta

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

imagem.png

Nó de árvore vermelho-preto 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;
}

Nó de espaço reservado: Nó herdado. Quando o cálculo é necessário ( computermétodo de uso), primeiro use o nó de espaço reservado para ocupar o lugar. Após o cálculo, o nó é construído para substituir o nó de espaço reservado.

	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;
        }
    }

Princípio de implementação

estrutura

Durante a construção, os parâmetros de entrada são verificados, em seguida, a capacidade da tabela hash é calculada com base na capacidade de dados e no fator de carga a ser armazenado e, finalmente, a capacidade da tabela hash é ajustada à potência de 2.

Não é inicializado durante a construção, mas espera até ser usado antes de criá-lo (carregamento lento)

	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;
    }

ler-obter

O cenário de leitura usa volátil para garantir a visibilidade.Mesmo as modificações de outros threads são visíveis e não há necessidade de usar outros meios para garantir a sincronização.

A operação de leitura precisa encontrar elementos na tabela hash, embaralhar o valor hash por meio do algoritmo de perturbação e, em seguida, usar o valor hash para obter o índice por meio do algoritmo hash. Ele é dividido em múltiplas situações de acordo com o primeiro nó do índice .

  1. O algoritmo de perturbação interrompe totalmente o valor do hash (para evitar causar muitos conflitos de hash) e o bit de sinal &0 garante que o resultado seja positivo.

    int h = spread(key.hashCode())

    Algoritmo de perturbação: operação XOR de 16 bits de valores de hash altos e baixos

    Após o algoritmo de perturbação, &HASH_BITS = 0x7fffffff (011111...), o bit de sinal é 0 para garantir que o resultado seja um número positivo

    Valores de hash negativos representam funções especiais, como nós de encaminhamento, primeiros nós de árvores, nós de espaço reservado, etc.

    	static final int spread(int h) {
            return (h ^ (h >>> 16)) & HASH_BITS;
        }
    
  2. Use o valor hash embaralhado para obter o índice (subscrito) na matriz por meio do algoritmo hash

    n é o comprimento da tabela hash:(n = tab.length)

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

    h é o valor hash calculado e a posição do índice pode ser encontrada pelo valor hash% (comprimento da tabela hash - 1)

    Para melhorar o desempenho, é estipulado que o comprimento da tabela hash seja a n potência de 2. O comprimento da tabela hash em binário deve ser 1000...., e o (n-1)comprimento em binário deve ser 0111...

    Portanto, (n - 1) & hao calcular o índice, o resultado da operação AND deve estar entre 0 e n - 1. Use operações de bits para melhorar o desempenho.

  3. Depois de obter os nós do array, você precisa compará-los

    Depois de encontrar o primeiro nó na tabela hash, compare a chave para ver se é o nó atual.

    Regras de comparação: compare os valores de hash primeiro. Se os valores de hash do objeto forem iguais, pode ser o mesmo objeto. Você também precisa comparar a chave (== e igual). Se os valores de hash forem não é o mesmo, então definitivamente não é o mesmo objeto.

    A vantagem de comparar os valores de hash primeiro é melhorar o desempenho da pesquisa . Se igual for usado diretamente, a complexidade do tempo pode aumentar (como igual a String).

  4. O método de endereço em cadeia é usado para resolver conflitos de hash, portanto, depois de encontrar o nó, você pode percorrer a lista ou árvore vinculada; devido à expansão da tabela de hash, você também pode ter que procurar um novo nó.

    4.1 O primeiro nó é relativamente bem-sucedido e retorna diretamente

    4.2 O valor hash do primeiro nó é negativo, indicando que o nó é um caso especial: nó de encaminhamento, primeiro nó da árvore, nó reservado de reserva calculada

    • Se for um nó de encaminhamento e estiver sendo expandido, vá para o novo array para encontrá-lo.
    • Se for TreeBin, pesquise na árvore rubro-preta
    • Se for um nó de espaço reservado, retorne vazio diretamente.

    4.3 Percorra a lista vinculada e compare sequencialmente

Obter 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;
}

escrever

Ao adicionar elementos, use o método de sincronização CAS+sincronizado (bloqueando apenas um determinado primeiro nó na tabela hash) para garantir a atomicidade.

  1. Obtenha o valor do hash: algoritmo de perturbação + certifique-se de que o valor do hash seja positivo
  2. A tabela hash está vazia, o CAS garante uma inicialização do thread
	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. Passe o valor hash através do algoritmo hash para obter o nó no índicef = tabAt(tab, i = (n - 1) & hash)

  2. Manuseie de acordo com diferentes situações

    • 4.1 Quando o primeiro nó está vazio, o CAS adiciona diretamente o nó à posição do índice.casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))

      imagem.png

    • 4.2 Quando o hash do primeiro nó é MOVED -1, significa que o nó é um nó de encaminhamento, indicando que está se expandindo, ajudando a expandir a capacidade.

    • 4.3 Bloqueio do primeiro nó

      • 4.3.1 Percorra a lista vinculada para localizar e adicionar/substituir

        imagem.png

      • 4.3.2 Percorra a árvore para localizar e adicionar/sobrescrever

  3. addCountConte os dados de cada nó e verifique a expansão

colocar código

//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;
}

Expansão

Para evitar conflitos de hash frequentes, quando o número de elementos na tabela hash/o comprimento da tabela hash exceder o fator de carga, expanda a capacidade (aumente o comprimento da tabela hash)

De modo geral, a expansão consiste em aumentar o comprimento da tabela hash em 2 vezes. Por exemplo, de 32 a 64, o comprimento é garantido como uma potência de 2; se o comprimento da expansão atingir o limite superior do tipo inteiro, o o valor inteiro máximo é usado.

Quando ocorre a expansão, a lista vinculada ou árvore em cada slot do array precisa ser migrada para o novo array.

Se o processador for multi-core, essa operação de migração não será concluída apenas por um thread, mas outros threads também ajudarão na migração.

Durante a migração, deixe cada thread migrar vários slots da direita para a esquerda. Após a conclusão da migração, será avaliado se todas as migrações foram concluídas. Caso contrário, a migração continuará de maneira circular.

A operação de expansão ocorre principalmente no transfermétodo, e a expansão ocorre principalmente em três cenários:

  1. addCount: Após adicionar o nó, aumente a contagem e verifique a expansão.
  2. helpTransfer: Quando o thread é colocado, verifica-se que ele está migrando, para ajudar a ampliar a capacidade.
  3. tryPresize: Tente ajustar a capacidade (adição em lote putAll, chamada quando o comprimento da matriz da árvore não excede 64 treeifyBin)

Dividido nas 3 etapas a seguir

  1. Calcule quantos slots migrar de cada vez com base no número de núcleos da CPU e no comprimento total da tabela hash, o mínimo é 16

  2. A nova tabela hash está vazia, indicando que foi inicializada.

  3. Migração circular

    • 3.1 Aloque o intervalo [bround,i] responsável pela migração (pode haver migração simultânea de múltiplas threads)

      imagem.png

    • 3.2 Migração: dividida em migração de lista vinculada e migração de árvore

      Migração de lista vinculada

      1. Faça hash completo dos nós da lista vinculada no índice da nova tabela hash, índice + os dois subscritos do comprimento da tabela hash antiga (semelhante ao HashMap)

      2. Coloque o nó na lista vinculada da posição do índice (hash e comprimento da tabela hash), o resultado é 0 na posição do índice da nova matriz, o resultado é 1 na posição do novo índice da matriz + o comprimento da antiga tabela hash

        imagem.png

        Por exemplo, o comprimento da tabela hash antiga é 16. No índice 3, o valor binário de 16 é 10.000, hash&16 => hash& 10.000. Ou seja, se o quinto bit do valor hash do nó for 0, é colocado na posição 3 da nova tabela hash. Se for 1, coloque-o no subscrito 3+16 da nova tabela hash.

      3. Use o método de interpolação head para construir uma lista vinculada ln quando o resultado do cálculo for 0, e uma lista vinculada hn quando for 1. Para facilitar a construção da lista vinculada, o nó lastRun será encontrado primeiro: o nó lastRun e os nós subsequentes são todos nós da mesma lista vinculada, o que facilita a migração.

        Construa lastRun primeiro antes de construir a lista vinculada, por exemplo, lastRun e->f na figura, primeiro coloque lastRun na lista vinculada ln, depois percorra a lista vinculada original, atravesse para a: a->e->f, atravesse para b: b->a ->e->f

      imagem.png

      1. Após a migração de cada posição do índice, o nó de encaminhamento é definido para a posição correspondente na tabela hash original.Quando outros threads realizam operações de leitura e obtenção, eles pesquisam na nova tabela hash de acordo com o nó de encaminhamento e realizam operações de gravação e colocação em ajudar a expandir a capacidade (migração de outros intervalos)

        imagem.png

Código de expansão

//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;
                        }
                    }
                }
            }
        }
    }

O princípio de implementação não descreve muito sobre árvores rubro-negras. Por um lado, existem muitos conceitos de árvores rubro-negras e, por outro lado, quase os esqueci (já estou velho e posso ' não escrevo árvores rubro-negras à mão como na faculdade)

Outro aspecto é: acho que basta conhecer os benefícios do uso de árvores rubro-negras, e não é muito usado no trabalho, mesmo que a árvore rubro-negra mude de cor, gire para a esquerda ou para a direita para atender às condições do vermelho -árvore negra, não tem sentido., os alunos interessados ​​​​podem simplesmente ir estudar.

Iterador

Iteradores em ConcurrentHashMap são fracamente consistentes e usam a tabela hash registrada para reconstruir novos objetos na busca

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 chave

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 usa a estrutura de dados de uma tabela hash. Quando ocorre um conflito de hash, ele usa o método de endereço de cadeia para resolvê-lo. Os nós com hash no mesmo índice são construídos em uma lista vinculada. Quando a quantidade de dados atinge um determinado limite, a lista vinculada é convertida em uma árvore rubro-negra.

ConcurrentHashMap usa modificação volátil para armazenar dados, tornando modificações em outros threads visíveis no cenário de leitura. Não há necessidade de usar um mecanismo de sincronização. CAS e synchronzied são usados ​​para garantir a atomicidade no cenário de escrita.

Ao consultar dados com get, primeiro passe o valor hash da chave através do algoritmo de perturbação (XOR alto e baixo de 16 bits) e certifique-se de que o resultado seja um número positivo (com o bit de sinal superior 0) e, em seguida, combine-o com o comprimento da tabela hash superior -1 para encontrar o valor do índice, depois de encontrar o índice, pesquise de acordo com diferentes situações (a comparação determina primeiro o valor do hash e, em seguida, determina a chave se for igual)

Ao adicionar/sobrescrever dados em put, a posição do índice é primeiro encontrada através do algoritmo de perturbação e hash, e depois pesquisada de acordo com diferentes situações. Se encontrada, será sobrescrita, e se não for encontrada, será substituída.

Quando a expansão é necessária, o intervalo de slot que precisa ser migrado será organizado para o thread. Quando outros threads executam put, eles também ajudarão na migração. Cada vez que um thread migra um slot, o nó de encaminhamento será definido para o original tabela hash, para que haja consultas de thread. Você pode pesquisar a nova tabela hash através do nó de encaminhamento. Quando todos os slots forem migrados, deixe um thread para definir a tabela hash, quantidade, etc.

O iterador usa consistência fraca e um novo objeto é construído por meio de uma tabela hash ao obter o iterador.

ConcurrentHashMap garante apenas a segurança relativa do thread, mas não pode garantir a segurança absoluta do thread. Se você precisar realizar uma série de operações, deverá usá-lo corretamente.

Este artigo foi publicado pelo OpenWrite, um blog que publica vários artigos !

Lei Jun: A versão oficial do novo sistema operacional da Xiaomi, ThePaper OS, foi empacotada. Uma janela pop-up na página da loteria Gome App insulta seu fundador. O governo dos EUA restringe a exportação da GPU NVIDIA H800 para a China. A interface do Xiaomi ThePaper OS é exposto. Um mestre usou Scratch para esfregar o simulador RISC-V e ele foi executado com sucesso. Kernel Linux RustDesk Remote Desktop 1.2.3 lançado, suporte aprimorado a Wayland Depois de desconectar o receptor USB da Logitech, o kernel Linux travou DHH revisão precisa de "ferramentas de empacotamento ": o front-end não precisa ser construído (Sem Build) JetBrains lança Writerside para criar documentação técnica Ferramentas para Node.js 21 lançadas oficialmente
{{o.nome}}
{{m.nome}}

Acho que você gosta

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