Capítulo adicional sobre el camino de crecimiento de los programadores: Introducción a ConcurrentHashmap

El último artículo adicional presentó el principio de implementación de hashmap, por lo que esta vez presentaré el principio de implementación de concurrentHashmap.

¿Qué es el Hashmap concurrente?

Como su nombre lo indica, concurrent = ocurre al mismo tiempo, por lo que concurrentHashmap probablemente pueda traducirse a hashmap concurrente. De hecho, se puede utilizar para manejar escenarios de alta concurrencia. Si Hashmap es una caja de almacenamiento pública sin contraseña, entonces ConcurrentHashmap es una caja de almacenamiento con candado. La característica más importante de concurrentHashmap es la tecnología de segmentación de bloqueo, es decir, el segmento, que es su característica más importante a diferencia de Hashmap. Además, concurrentHashmap modifica la entrada de su unidad de almacenamiento básica y algunos de sus parámetros se modifican con volátil para garantizar su visibilidad. (versión jdk7.0)

¿Por qué utilizamos ConcurrentHashmap?

En la última introducción a Hashmap, presentamos brevemente el problema de Hashmap, es decir, en el caso de concurrencia, habrá una lista circular enlazada que provocará la aparición de un bucle infinito . Después de estos días de investigación, tengo una comprensión más profunda de las razones del surgimiento de las listas circulares enlazadas. (la versión es jdk7)

En primer lugar, sabemos que hashmap es una matriz de listas vinculadas y la solución a los conflictos de hash es el método de direcciones en cadena. Como se muestra en la figura,
inserte la descripción de la imagen aquí
en circunstancias normales no habrá problemas con esta estructura de almacenamiento, las condiciones para que un problema provoque un bucle son las siguientes:

  1. acceso simultáneo
  2. Necesidad de expandirse

Echemos un vistazo al código fuente:

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

Lo anterior es el código fuente de poner un 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);
}

Lo anterior es el código fuente para agregar un nuevo nodo de entrada. Tenga en cuenta que el nodo se agrega primero y luego se expande. ConcurrentHashmap primero juzga si se requiere expansión. Si se requiere expansión, primero expanda y luego agregue nodos de entrada. Este beneficio puede evitar una expansión no válida (es decir, no se agregan elementos de nodo después de la expansión).

 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 es el código de expansión de la versión jdk7.0, que se divide principalmente en tres pasos:
1.0 - crear una nueva matriz
1.1 - copiar los datos en la matriz original
1.2 - redefinir el umbral (umbral)

Mira el código de transferencia nuevamente.

    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;//注意这里,进行下一次的插入
            }
        }
    }

El código aquí es más interesante: inserta los valores de la matriz original en la nueva matriz mediante la interpolación del encabezado. A continuación miramos una imagen,
inserte la descripción de la imagen aquí

El hilo 1 aún no ha comenzado a expandirse pero está listo para expandirse, es decir, ha alcanzado la posición del código del paso 1.0. En este momento llega el maldito hilo 2. Dado que la tabla en la función de transferencia es pública, es decir, cada hilo tiene una copia de seguridad de la matriz original, y los objetos señalados por e en la tabla son todos el mismo objeto. . Con esta premisa, veamos la siguiente imagen.
inserte la descripción de la imagen aquí

El subproceso 2 ha completado la expansión y el subproceso 1 comienza a expandirse en este momento (esto es solo una situación accidental, el término técnico se llama condición de carrera), luego el subproceso 1 apunta al nodo A al principio y A.next -> nueva matriz después de ejecutar la siguiente declaración En B, está B.next -> A, por lo que hay un enlace de anillo.
inserte la descripción de la imagen aquí
Después de que aparezca la lista circular enlazada, habrá un bucle infinito en la lectura posterior (obtener).
Además, después de que el elemento no NULL del hashmap se coloque en subprocesos múltiples, la operación de obtención obtendrá un valor NULL; La operación de colocación roscada provocará la pérdida del elemento. Los técnicos interesados ​​pueden comprobarlo por sí mismos.

En comparación con el problema de hashmap, concurrentHashmap tiene más ventajas: primero, admite el acceso concurrente, también conocido como contenedor concurrente, y segundo, primero juzga los elementos de almacenamiento después de la expansión para evitar una expansión no válida (el problema de expandir pero no insertar nodos). y nuevamente, concurrentHashmap optimiza la estructura de entrada y modifica el valor y el siguiente con volátil para mantener la visibilidad. El punto más poderoso es que concurrentHashmap utiliza tecnología de segmentación de bloqueo, de modo que cada segmento (segmento) está equipado con un bloqueo, de modo que se puede acceder a diferentes segmentos al mismo tiempo. (JDK 7)

Análisis del código fuente de ConcurrentHashmap

inserte la descripción de la imagen aquí
El diagrama de clases de ConcurrentHashmap en jdk7 se muestra aproximadamente en la figura anterior, y el análisis del código fuente comienza a continuación:
primero mire el código de hashEntry

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

Tenga en cuenta que el valor aquí se modifica con volátil para garantizar su visibilidad, y otras variables miembro se modifican con final para evitar que se destruya la estructura de la lista vinculada.

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; //负载因子
}

Después de comprender la estructura del nodo, echemos un vistazo al método de inicialización en concurrentHashmap.

Inicializar Hashmap concurrente


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

En esa inicialización hay algunos parámetros:

  1. factor de carga factor de carga
  2. initialCapacity Tamaño de capacidad inicial, igual a segmento * capacidad de segmento
  3. concurrencyLevel nivel de concurrencia, utilizado para determinar la longitud del segmento, como concurrencyLevel = 13,14,15,16 cuando el tamaño del segmento es 16
  4. sshift indica el número de dígitos ocupados por el nivel de concurrencia (número de segmentos), que se utiliza para determinar el tamaño del desplazamiento del segmento. Desplazamiento del segmento = 32: sshift indica el número de dígitos desplazados hacia la derecha durante el hash más adelante, lo cual se discutirá más adelante
  5. ssize indica el tamaño del segmento, que no es inferior a la potencia de 2 del nivel de concurrencia.
  6. El desplazamiento del segmento segmentShift, que se mencionará más adelante, se utiliza para repetir el segmento.
  7. segmentMask La máscara de segmento, que se mencionará más adelante, se utiliza para repetir segmentos para tomar los n bits más altos.
  8. MAXIMUM_CAPACITY es el número máximo de segmentos
  9. c, cap se usa para determinar la capacidad de cada segmento, que también es una potencia de 2, y el factor de carga también es aplicable a los objetos en cada segmento.

Introducción al proceso de inicialización:

  • Realizar validación de parámetros
  • Determine si el nivel de concurrencia excede el valor máximo y, de ser así, establezca el nivel de concurrencia en el valor máximo.
  • Obtenga ssize (longitud del segmento) y sshift según el nivel de concurrencia
  • Calcule el desplazamiento de segmento (desplazamiento de segmento) = 32 - sshift, y luego determine el desplazamiento de datos de orden superior que debe agregarse con AND al repetir (la cantidad de bits que los bits de orden superior se mueven hacia la derecha, lo que hace que los bits de orden superior sean bajos ).
  • Calcule la máscara de segmento (máscara de segmento) = tamaño -1, es decir, tome los bits inferiores de la máscara de segmento después del desplazamiento para repetir. (Es decir, los datos del bit alto original n pueden determinar la posición del segmento)
  • Calcule la capacidad de hashEntry en cada segmento. Es cap. Por defecto, capacidad inicial es igual a 16 y factor de carga es igual a 0,75. Al calcular cap=1, umbral=0.

Insertar elementos en concurrentHashmap

Primero, echemos un vistazo a la estructura de la
fuente de la imagen del segmento:
https://blog.csdn.net/m0_37135421/article/details/80551884
inserte la descripción de la imagen aquí

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操作保证并发安全
}

El segmento utiliza un bloqueo reentrante para garantizar que cada operación en el segmento sea atómica. Cada vez que se opera un segmento, primero se adquiere el bloqueo del segmento y luego se realiza la operación. Y las operaciones entre segmentos no interfieren entre sí debido a la existencia de diferentes bloqueos.

Echemos un vistazo al método de venta.

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

Los pasos de la operación de venta:

  1. Determinar si el valor está vacío.
  2. hash la clave
  3. Determine la ubicación del segmento (segmento) del almacén de datos en función del valor hash de la clave
  4. Inserte la clave, el par clave-valor en hashEntry en el segmento, devuelva el valor anterior si existe y cree un nuevo nodo si no existe. Tenga en cuenta que el candado se inserta aquí.

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

La operación de obtención es relativamente simple, siempre que la posición del segmento se determine de acuerdo con el valor repetido y luego la posición de hashentry se determine de acuerdo con el par clave-valor.

¿Cómo logra concurrentHashmap la expansión?

  1. Primero juzgue si la matriz hashentry en el segmento alcanza el umbral, si lo excede, expanda la capacidad y luego inserte elementos
  2. La expansión es generalmente 2 veces mayor y los elementos de la matriz original se vuelven a aplicar hash y luego se insertan en la nueva matriz. Para mayor eficiencia, concurrentHashmap solo expande un segmento pero no todo el contenedor.
    -------------------------- Continuará --------------------- --------

Supongo que te gusta

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