[Contenedor concurrente] 2 ConnrentHashMap

1 La evolución de los contenedores concurrentes

Seguridad de subprocesos de conversión de clases de herramientas
Collections.synchronizedList (lista) adopta la forma de bloques de código sincronizados internamente y se bloquea para garantizar la seguridad de subprocesos.

public E get(int index) {    synchronized (mutex) {return list.get(index);}}

Vectior y HashTable usan un método sincrónico para bloquear.

En la mayoría de los casos, ConcurrentHashMap y CopyOnWriteArrayList funcionan mejor. CopyOnWriteArrayList es adecuado para leer más y escribir menos, si la lista ha sido modificada, puede ser en forma de Colecciones.

  1. ConcurrentHashMap: HashMap seguro para subprocesos
  2. CopyOnWriteArrayList: Lista segura para subprocesos
  3. BlockingQueue: interfaz, cola de bloqueo, muy adecuada para canales de intercambio de datos
  4. ConCurrentLinkedQueue: una cola concurrente sin bloqueo eficiente, implementada mediante una lista enlazada. Puede considerarse como una LinkedList segura para subprocesos

2 ConnrentHashMap

2.1 HashMap

¿Por qué no es seguro el hilo de HashMap?

  1. Los subprocesos chocan con las operaciones de venta al mismo tiempo, lo que resulta en la pérdida de datos;
  2. La pérdida de datos ocurre cuando los hilos se colocan y expanden al mismo tiempo.
  3. El bucle infinito provoca el 100% de la CPU. Porque en la expansión concurrente de subprocesos múltiples, el método de cambio de tamaño transer puede generar una lista enlazada circular, lo que conduce a un bucle sin fin. https://coolshell.cn/articles/9606.html

2.2 El principio de realización de ConcurrentHashMap

La estructura de la versión 1.7, la capa más externa está configurada con múltiples segmentos, y la estructura subyacente de cada segmento es similar a HashMap, que sigue siendo un método de cremallera compuesto por matrices y listas vinculadas. Cada segmento tiene un bloqueo ReentrantLock independiente, que no se afecta entre sí y mejora la eficiencia de la concurrencia. El valor predeterminado es 16 segmentos, lo que significa que se admiten 16 subprocesos para escritura simultánea. Se establece cuando se inicializa el segmento y no se puede modificar una vez establecido.

Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí

Versión 1.8, la capa inferior adopta la implementación sincronizada de nodo + CAS +. La estructura es similar al HashMap de la versión JDK1.8 En comparación con la versión 1.7, la granularidad del bloqueo es menor y cada nodo tiene la capacidad de concurrencia.

El análisis del código fuente se refiere principalmente al método put / get.

final V putVal(K key, V value, boolean onlyIfAbsent) {
    
    
        // key和value不允许为空
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        // 记录链表长度
        int binCount = 0;
        // 循环遍历数组
        for (Node<K,V>[] tab = table;;) {
    
    
            Node<K,V> f; int n, i, fh; K fk; V fv;
            // 数组为空,初始化数组
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 数组索引位置桶为空,CAS初始化Node
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    
    
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                    break;                   // no lock when adding to empty bin
            }
            // 数组正在resize扩容,则帮助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            // 找到值并且相同,则直接返回
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null)
                return fv;
            else {
    
     // 采用synchronized内置锁写入数据
                V oldVal = null;
                synchronized (f) {
    
    
                    if (tabAt(tab, i) == f) {
    
    
                        if (fh >= 0) {
    
    
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
    
    
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
    
    
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
    
    
                                    pred.next = new Node<K,V>(hash, key, value);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
    
    
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
    
    
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
    
    
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

la diferencia:

  1. Estructura de datos: reflejada principalmente en el grado de concurrencia
  2. Colisión de hash: 1.7 adopta el método de cremallera y 1.8 adopta la forma de conversión de árbol rojo-negro de HashMap.
  3. Herramientas de concurrencia: 1.7 usa ReentrantLock, 1.8 usa sincronizado y CAS.

2.3 Problemas a los que se debe prestar atención al usar

El uso de ConcurrentHashMap solo puede garantizar que las operaciones put y get del mapa sean seguras para subprocesos, mientras que las operaciones combinadas no pueden garantizar la seguridad para subprocesos, como a ++.

public class OptionsNotSafe implements Runnable {
    
    
    /**
     * map容器
     */
    private static ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
    
    
        map.put("sum", 0);
        Thread thread1 = new Thread(new OptionsNotSafe());
        Thread thread2 = new Thread(new OptionsNotSafe());
        thread1.start();
        thread2.start();
    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 10000; i++) {
    
    
            Integer old = map.get("sum");
            map.put("sum", old + 1);
        }
    }
}

Se puede resolver mediante un bloque de código de sincronización sincronizado, pero destruirá la idea de ConcurrentHashMap.

    public void run() {
    
    
        for (int i = 0; i < 10000; i++) {
    
    
            while (true){
    
    
                Integer old = map.get("sum");
                Integer newVal = old + 1;
                if (map.replace("sum", old, newVal)) {
    
    
                    break;
                }
            }
        }
    }

Reemplazar usa la idea de giro y devuelve verdadero solo cuando la actualización es exitosa.

Supongo que te gusta

Origin blog.csdn.net/LIZHONGPING00/article/details/114274591
Recomendado
Clasificación