[Concurrent container] 2 ConnrentHashMap

1 The evolution of concurrent containers

Tool class conversion thread safety
Collections.synchronizedList(list) adopts the form of synchronized code blocks internally, and locks to ensure thread safety.

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

Vectior and HashTable use a synchronous method to lock.

In most cases, ConcurrentHashMap and CopyOnWriteArrayList perform better. CopyOnWriteArrayList is suitable for reading more and writing less. If the list has been modified, it can be in the form of Collections.

  1. ConcurrentHashMap: thread-safe HashMap
  2. CopyOnWriteArrayList: Thread-safe List
  3. BlockingQueue: interface, blocking queue, very suitable for data sharing channels
  4. ConCurrentLinkedQueue: An efficient non-blocking concurrent queue, implemented using a linked list. Can be regarded as a thread-safe LinkedList

2 ConnrentHashMap

2.1 HashMap

Why is HashMap thread unsafe?

  1. Threads collide with put operations at the same time, resulting in data loss;
  2. Data loss occurs when threads are put and expanded at the same time.
  3. The infinite loop causes 100% of the CPU. Because in the multi-threaded concurrent expansion, the transer method of resize may generate a circular linked list, which leads to an endless loop. https://coolshell.cn/articles/9606.html

2.2 The realization principle of ConcurrentHashMap

The structure of version 1.7, the outermost layer is set with multiple segments, and the underlying structure of each segment is similar to HashMap, which is still a zipper method composed of arrays and linked lists. Each segment has an independent ReentrantLock lock, which does not affect each other and improves concurrency efficiency. The default is 16 Segments, which means that 16 threads are supported for concurrent writing. It is set when the segment is initialized, and cannot be modified after it is set.

Insert picture description here
Insert picture description here

Version 1.8, the bottom layer adopts node+CAS+synchronized implementation. The structure is similar to the HashMap of the JDK1.8 version. Compared with the 1.7 version, the lock granularity is smaller, and each node has the concurrency capability.

Source code analysis mainly refers to the put/get method.

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

the difference:

  1. Data structure: mainly reflected in the degree of concurrency
  2. Hash collision: 1.7 adopts the zipper method, and 1.8 adopts the red-black tree conversion form of HashMap.
  3. Concurrency tools: 1.7 uses ReentrantLock, 1.8 uses synchronized and CAS.

2.3 Problems that need to be paid attention to when using

Using ConcurrentHashMap can only ensure that the put and get operations of the map are thread-safe, while combined operations cannot guarantee thread-safety, such as 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);
        }
    }
}

It can be solved by synchronized synchronization code block, but it will destroy the idea of ​​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;
                }
            }
        }
    }

Replace uses the idea of ​​spin, and returns true only when the update is successful.

Guess you like

Origin blog.csdn.net/LIZHONGPING00/article/details/114274591