Capítulo extra sobre o caminho de crescimento dos programadores – Introdução ao ConcurrentHashmap

O último artigo extra introduziu o princípio de implementação do hashmap, então desta vez apresentarei o princípio de implementação do concurrentHashmap.

O que é Hashmap simultâneo?

Como o nome indica, concurrent = acontece ao mesmo tempo, então concurrentHashmap provavelmente pode ser traduzido em hashmap simultâneo. Na verdade, ele pode ser usado para lidar com cenários de alta simultaneidade. Se Hashmap for uma caixa de armazenamento pública sem senha, então ConcurrentHashmap será uma caixa de armazenamento com cadeado. A maior característica do concurrentHashmap é a tecnologia de segmentação de bloqueio, ou seja, segmento, que é sua maior característica diferente do Hashmap. Além disso, concurrentHashmap modifica sua entrada básica da unidade de armazenamento e alguns de seus parâmetros são modificados com voláteis para garantir sua visibilidade. (versão jdk7.0)

Por que usamos ConcurrentHashmap?

Na última introdução ao Hashmap, apresentamos brevemente o problema do Hashmap, ou seja, no caso de simultaneidade, haverá uma lista vinculada circular levando ao aparecimento de um loop infinito . Após esses dias de pesquisa, tenho uma compreensão mais profunda das razões do surgimento das listas vinculadas circulares. (a versão é jdk7)

Em primeiro lugar, sabemos que hashmap é um array de listas vinculadas, e a solução para conflitos de hash é o método de endereço em cadeia. Como mostrado na figura,
insira a descrição da imagem aqui
em circunstâncias normais, não haverá problemas com esta estrutura de armazenamento.As condições para um problema causar um loop são as seguintes:

  1. acesso simultâneo
  2. Precisa expandir

Vamos dar uma olhada no código fonte:

public V put(K key, V value) {
    
    
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key.hashCode());
    // 找节点位置
    int i = indexFor(hash, table.length);
    //遍历数组,查找到了就返回原值如果没有就添加新entry节点
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    
    
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    
    
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(hash, key, value, i); // 注意这里,进行新的节点的添加
    return null;
}

O acima é o código fonte de colocar um elemento

void addEntry(int hash, K key, V value, int bucketIndex) {
    
    
    Entry<K,V> e = table[bucketIndex];
    // 新增一个节点并将节点头指针改为这个新增的节点,因为第四个参数表示next对象
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold) //注意这里,进行扩容
        resize(2 * table.length);
}

O texto acima é o código-fonte para adicionar um novo nó de entrada. Observe que o nó é adicionado primeiro e depois expandido. ConcurrentHashmap primeiro avalia se a expansão é necessária. Se a expansão for necessária, primeiro expanda e depois adicione nós de entrada. Esse benefício pode evitar expansão inválida (ou seja, nenhum elemento de nó é adicionado após a expansão)

 void resize(int newCapacity) {
    
    
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
    
    
            threshold = Integer.MAX_VALUE;
            return;
        }
        Entry[] newTable = new Entry[newCapacity]; //1.0
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash); //1.1
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);  //1.2
}

Este é o código de expansão da versão jdk7.0, que é dividido principalmente em três etapas:
1.0 - criar um novo array
1.1 - copiar os dados no array original
1.2 - redefinir o limite (limite)

Veja o código de transferência novamente

    void transfer(Entry[] newTable, boolean rehash) {
    
    
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
    
    
            while(null != e) {
    
    
                Entry<K,V> next = e.next; //step 1.0
                if (rehash) {
    
    
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);//查找位置
                e.next = newTable[i];//注意这里,将节点指向新节点
                newTable[i] = e;//注意这里,头插法插入新数组
                e = next;//注意这里,进行下一次的插入
            }
        }
    }

O código aqui é mais interessante: ele insere os valores do array original no novo array por meio de interpolação de cabeçalho. Em seguida, olhamos para uma foto,
insira a descrição da imagem aqui

O thread 1 ainda não começou a se expandir, mas está pronto para se expandir, ou seja, atingiu a posição de código da etapa 1.0. Nesse momento chega o maldito thread 2. Como a tabela na função de transferência é pública, ou seja, cada thread tem um backup do array original, e os objetos apontados por e na tabela são todos o mesmo objeto . Com essa premissa, vejamos a imagem a seguir
insira a descrição da imagem aqui

O thread 2 completou a expansão e o thread 1 começa a se expandir neste momento (esta é apenas uma situação acidental, o termo técnico é chamado de condição de corrida), então o thread 1 aponta para o nó A no início e A.next -> novo array após executar a seguinte instrução Em B, há B.next -> A, então há um link de anel.
insira a descrição da imagem aqui
Depois que a lista vinculada circular aparecer, haverá um loop infinito na leitura subsequente (get).
Além disso, após o elemento multi-threaded put não NULL do hashmap, a operação get obterá um valor NULL; A operação de colocação encadeada fará com que o elemento seja perdido. Os técnicos interessados ​​​​podem conferir por conta própria.

Comparado com o problema do hashmap, concurrentHashmap tem mais vantagens. Primeiro, ele suporta acesso simultâneo, também conhecido como contêiner simultâneo. Em segundo lugar, primeiro julga os elementos de armazenamento após a expansão para evitar expansão inválida (o problema de expandir, mas não inserir nós), e novamente, concurrentHashmap otimiza a estrutura de entrada e modifica o valor e próximo com volátil para manter a visibilidade. O ponto mais poderoso é que concurrentHashmap utiliza tecnologia de segmentação de bloqueio, de modo que cada segmento (segmento) seja equipado com um bloqueio, para que diferentes segmentos possam ser acessados ​​simultaneamente. (JDK 7)

Análise de código-fonte ConcurrentHashmap

insira a descrição da imagem aqui
O diagrama de classes do ConcurrentHashmap no jdk7 é mostrado aproximadamente na figura acima, e a análise do código-fonte começa abaixo:
primeiro observe o código do hashEntry

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

Observe que o valor aqui é modificado com volátil para garantir sua visibilidade, e outras variáveis ​​de membro são modificadas com final para evitar que a estrutura da lista vinculada seja destruída.

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    
    
    transient volatile int count; //Segment中元素的数量
    transient int modCount; // 修改次数
    transient int threshold;// 阈值(扩容临界值)
    transient volatile HashEntry<K,V>[] table; // hashentry节点数组
    final float loadFactor; //负载因子
}

Depois de entender a estrutura do nó, vamos dar uma olhada no método de inicialização em concurrentHashmap

Inicializar concurrentHashmap


    /* ---------------- Constants -------------- */

    /**
     * 最大的容量,是2的幂次方(java 数组索引和分配的最大值约为 1<<30,32位的hash值前面两位用于控制)
     */
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 数组容量,为2的幂次方, 
     * 1<=DEFAULT_CAPACITY<=MAXIMUM_CAPACITY
     */
    private static final int DEFAULT_CAPACITY = 16;

    /**
     * 最大的数组容量(被toArray和其他数组方法调用时获取所需要)
     */
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 默认的并发等级.
     * 为12、13、14、15、16表示segment数组大小默认为16
     */
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    /**
     * 负载因子,考虑到红黑树和链表的平均检索时间,取0.75为宜。
     * 这样接近O(1)
     */
    private static final float LOAD_FACTOR = 0.75f;

    /**
     * 红黑树化链表的阈值,即当前hashentry中桶链表节点的对象长度
     * >=8时进行扩容该节点会红黑树化
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 红黑树化链表的阈值,即当前hashentry中桶链表节点的对象长度
     * <= 6 时进行扩容该节点仍为链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 最小的链表数组容量(至少为4倍的TREEIFY_THRESHOLD。即32)
     * 以防止扩容和红黑树化阈值的冲突
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * Minimum number of rebinnings per transfer step. Ranges are
     * subdivided to allow multiple resizer threads.  This value
     * serves as a lower bound to avoid resizers encountering
     * excessive memory contention.  The value should be at least
     * DEFAULT_CAPACITY.
     */
    private static final int MIN_TRANSFER_STRIDE = 16;

    /**
     * 扩容戳,和resizeStamp函数有关
     * Must be at least 6 for 32bit arrays.(至少6位以满足32位的数组)
     * rs(RESIZE_STAMP_BITS) = 1 << (RESIZE_STAMP_BITS - 1)
     * rs(6) = 1 << (6-1) = 32
     */
    private static int RESIZE_STAMP_BITS = 16;

    /**
     * 最大的可扩容线程数
     * 线程在扩容时会将高RESIZE_STAMP_BITS作为扩容后的标记,高 32- RESIZE_STAMP_BITS 为作为扩容线程数
     */
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

    /**
     * The bit shift for recording size stamp in sizeCtl.
     * 扩容戳的位偏移
     */
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
	// ...

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    
    
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
        
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    while (ssize < concurrencyLevel) {
    
    
        ++sshift;
        ssize <<= 1;
    }
    segmentShift = 32 - sshift;
    segmentMask = ssize - 1;
    this.segments = Segment.newArray(ssize);

    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    //最小Segment中存储元素的个数为2
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
 	//创建segments数组并初始化第一个Segment,其余的Segment延迟初始化
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); 
    this.segments = ss;
}

Nessa inicialização existem alguns parâmetros:

  1. fator de carga loadFactor
  2. inicialCapacity Tamanho da capacidade inicial, igual ao segmento * capacidade do segmento
  3. concurrencyLevel nível de simultaneidade, usado para determinar o comprimento do segmento, como concurrencyLevel = 13,14,15,16 quando o tamanho do segmento é 16
  4. sshift indica o número de dígitos ocupados pelo nível de simultaneidade (número de segmentos), que é usado para determinar o tamanho do deslocamento do segmento. Deslocamento do segmento = 32 - sshift indica o número de dígitos deslocados para a direita durante o hash posterior, o que será discutido mais adiante
  5. ssize indica o tamanho do segmento, que não é inferior à potência de 2 do concurrencyLevel.
  6. segmentShift segmento offset, que será mencionado mais tarde, é usado para rehashing de segmento
  7. segmentMask A máscara de segmento, que será mencionada posteriormente, é usada para rehashing de segmento para obter n bits altos.
  8. MAXIMUM_CAPACITY é o número máximo de segmentos
  9. c, cap é usado para determinar a capacidade de cada segmento, que também é uma potência de 2, e o fator de carga também é aplicável aos objetos em cada segmento.

Introdução ao processo de inicialização:

  • Realizar validação de parâmetros
  • Determine se o nível de simultaneidade excede o valor máximo e, em caso afirmativo, defina o nível de simultaneidade para o valor máximo,
  • Obtenha ssize (comprimento do segmento) e sshift de acordo com o nível de simultaneidade
  • Calcule segmentshift (deslocamento de segmento) = 32 - sshift e, em seguida, determine o deslocamento de dados de ordem superior que precisa ser ANDed durante o rehashing (o número de bits que os bits de ordem superior movem para a direita, tornando os bits de ordem superior baixos ).
  • Calcule segmentmask (segment mask) = ssize -1, ou seja, pegue os bits inferiores da segmentmask após o deslocamento para rehashing. (Ou seja, os dados do bit alto original n podem determinar a posição do segmento)
  • Calcule a capacidade de hashEntry em cada segmento. É cap. Por padrão, inicialCapacity é igual a 16 e loadFactor é igual a 0,75. Ao calcular cap=1, o limite=0.

Inserir elementos em concurrentHashmap

Primeiro, vamos dar uma olhada na estrutura da
fonte da imagem do segmento:
https://blog.csdn.net/m0_37135421/article/details/80551884
insira a descrição da imagem aqui

static final class Segment<K, V> extends ReentrantLock implements Serializable {
    
    
 
	/**
	 * scanAndLockForPut中自旋循环获取锁的最大自旋次数。
	 */
	static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
 
	/**
	 * 链表数组,数组中的每一个元素代表了一个链表的头部
	 */
	transient volatile HashEntry<K, V>[] table;
 
	/**
	 * 用于记录每个Segment桶中键值对的个数
	 */
	transient int count;
 
	/**
	 * 对table的修改次数
	 */
	transient int modCount;
 
	/**
	 * 阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容
	 */
	transient int threshold;
 
	/**
	 * 负载因子,用于确定threshold,默认是1
	 */
	final float loadFactor;
}
 
static final class HashEntry<K, V> {
    
    
	final int hash;
	final K key;
	volatile V value; //设置可见性
	volatile HashEntry<K, V> next; //不再用final关键字,采用unsafe操作保证并发安全
}

O segmento usa um bloqueio reentrante para garantir que cada operação no segmento seja atômica. Cada vez que um segmento é operado, o bloqueio do segmento é adquirido primeiro e depois a operação é executada. E as operações entre segmentos não interferem entre si devido à existência de diferentes bloqueios.

Vamos dar uma olhada no método put

// ConcurrentHashMap类的put()方法
public V put(K key, V value) {
    
    
    Segment<K,V> s;
    //concurrentHashMap不允许key/value为空
    if (value == null)
        throw new NullPointerException();
    //hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
    int hash = hash(key);
    //返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment,即进行再散列操作
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject 
         (segments, (j << SSHIFT) + SBASE)) == null)        s = ensureSegment(j);
    // 调用Segment类的put方法
    return s.put(key, hash, value, false);  
}
 
// Segment类的put()方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    
    
    // 注意这里,这里进行加锁
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value); //如果加锁失败,则调用该方法
    V oldValue;
    try {
    
    
        HashEntry<K,V>[] tab = table;
        // 根据hash计算在table[]数组中的位置
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
    
    
            if (e != null) {
    
     //若不为null,则持续查找,知道找到key和hash值相同的节点,将其value更新
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
    
    
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
    
    
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
    
     //如果在链表中没有找到对应的node
                if (node != null) //如果scanAndLockForPut方法中已经返回的对应的node,则将其插入first之前
                    node.setNext(first);
                else //否则,new一个新的HashEntry
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                // 判断table[]是否需要扩容,并通过rehash()函数完成扩容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else  //设置node到Hash表的index索引处
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
    
    
        unlock();
    }
    return oldValue;
}

As etapas da operação put:

  1. Determine se o valor está vazio
  2. hash a chave
  3. Determine a localização do segmento (segmento) do armazenamento de dados com base no valor hash da chave
  4. Insira o par chave-valor chave-valor no hashEntry do segmento, retorne o valor antigo se existir e crie um novo nó se não existir. Observe que o cadeado é inserido aqui.

Obtenha elementos de concurrentHashmap

public V get(Object key) {
    
    
    Segment<K,V> s; 
    HashEntry<K,V>[] tab;
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    //先定位Segment,再定位HashEntry
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
    
    
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
    
    
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

A operação get é relativamente simples, desde que a posição do segmento seja determinada de acordo com o valor re-hashing e, em seguida, a posição hashentry seja determinada de acordo com o par chave-valor.

Como o concurrentHashmap consegue expansão?

  1. Primeiro julgue se a matriz hashentry no segmento atinge o limite, se exceder, expanda a capacidade e, em seguida, insira os elementos
  2. A expansão geralmente é uma expansão 2 vezes maior e os elementos do array original são re-hashing e então inseridos no novo array. Para maior eficiência, concurrentHashmap expande apenas um segmento, mas não o contêiner inteiro.
    -------------------------- Continua --------------------- --------

Acho que você gosta

Origin blog.csdn.net/qq_31236027/article/details/124504165
Recomendado
Clasificación