J.U.C package Summary - concurrent set

ConcurrentHashMap

Thread safety, key and value cannot be null

The underlying data structure is: data + linked list + red black tree CAS+Synchronized to ensure the security of concurrent updates

Several important variables

table: null by default. Initialization occurs in the first insert operation. The array with the default size of 16 is used to store Node data. When expanding, the size is always the power of 2.

nextTable: null by default. When expanding, the newly generated array is twice the size of the original array.

sizeCtl: the default value is 0, which is used to control the initialization and expansion of the table. The specific application will be reflected in the future.

  • -1 for table initializing
  • -N indicates that N-1 threads are expanding
  • Other information:
    1. If table is not initialized, it indicates the size of table to be initialized.
    2. If the table initialization is completed, it indicates the capacity of the table, which is 0.75 times of the table size by default. This formula is actually used to calculate 0.75 (n - (n > > 2)).

Node: data structure to save key, value and hash value of key. Value and next are decorated with volatile to ensure the visibility of concurrency.

ForwardingNode: a special Node node with a hash value of - 1, where a reference to nextTable is stored.

initialization

Make sure that the size of table is always the power of 2

    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

    private static final int tableSizeFor(int c) {
        int n = c - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

Note: do not initialize table directly, but delay to the first put operation

Table initialization

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            // If a thread finds sizectl < 0, it means that another thread performs CAS successfully, and the current thread only needs to give up cpu time slice
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -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;
    }

Sizecl is 0 by default. If there is a parameter passed during the instantiation of ConcurrentHashMap, sizecl will be a power of 2. So the thread executing the first put will execute Unsafe.compareAndSwapInt Method to modify sizeCtl to - 1, and only one thread can be modified successfully. Other threads pass the Thread.yield() let the CPU time slice wait for table initialization to complete.

Determine the index position of hash bucket array

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
(f = tabAt(tab, i = (n - 1) & hash)) == null

Put(K,V)

public V put(K key, V value) {
    return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException(); // Both key and value cannot be empty. If they are empty, an error will be thrown directly
    int hash = spread(key.hashCode());// Calculate Hash value and determine array subscript, which is the same as HashMap
    int binCount = 0;
    // Enter wireless loop until inserted
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // If the table is empty or the capacity is 0, it means there is no initialization
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();// Initialize array        
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // CAS if a bucket of the query array is empty, insert it directly into the bucket
	        if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))) // 
            break; // no lock when adding to empty bin
        }
        // If at the time of insertion, the node is in a forwordingNode state, indicating that capacity expansion is in progress, the current thread will help with capacity expansion
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // Lock head node
            synchronized (f) {
	            // Make sure that this node is indeed the header node in the array
                if (tabAt(tab, i) == f) {
                    // It's a linked list
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
				            // If you traverse to a value that is the same as the current key, change the value
                            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 you don't encounter the same key at the end of traversal, and there are no nodes behind, insert a key directly at the end
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
			        // If it is the storage of red black tree, it needs to be processed by the special processing of red black tree
                    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;
                        }
                    }
                }
            }
            if (binCount != 0) {
			    // Determine whether the number of nodes is greater than 8. If it is greater than 8, convert the linked list to a red black tree
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // Number of map s stored + 1
    addCount(1L, binCount);
    return null;
}

tabAt

    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

In the java memory model, we already know that each thread has a working memory, which stores a copy of the table. Although the table is decorated by volatile, it cannot guarantee that the thread will get the latest elements in the table every time, Unsafe.getObjectVolatile It can directly obtain the data of the specified memory to ensure that the data is up-to-date every time.

Expansion

When the capacity of a table is insufficient, that is, the number of elements in the table reaches the capacity threshold sizecl, and the table needs to be expanded.
The whole expansion is divided into two parts:

  1. Build a nextTable that is twice the size of a table.
  2. Copy the data of table to nextTable.

These two processes are very simple to implement in a single thread, but ConcurrentHashMap supports concurrent insertion, and there will be concurrency in the capacity expansion operation naturally. In this case, the second step can support concurrent replication of nodes, which naturally improves the performance, but the complexity of the implementation also rises a step.

The first step is to build nextTable. There is no doubt that only a single thread can initialize nextTable. The specific implementation is as follows:

    private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

adopt Unsafe.compareAndSwapInt Modify sizeCtl value to ensure that only one thread can initialize nextTable. The expanded array length is twice the original, but the capacity is 1.5.

The general idea of node moving from table to nextTable is the process of traversal and replication.

  1. First, we get the number of times i need to traverse according to the operation, then we use the tabAt method to get the element f of i position, and initialize a forwardNode instance fwd.
  2. i f f == null, put fwd in position i of table. This process adopts Unsafe.compareAndSwapObjectf The method realizes the concurrent movement of nodes.
  3. I f f is the head node of the linked list, construct a reverse order linked list, put them i n the positions of I and i+n of nextTable respectively, and move them to complete Unsafe.putObjectVolatile Method to assign fwd to the original table location.
  4. i f f is a TreeBin node, do a reverse order processing, and judge whether untreeify is needed. Put the processing results on the positions of i and i+n of nextTable respectively, and complete the movement. The same method is used Unsafe.putObjectVolatile Method to assign fwd to the original table location.

After traversing all the nodes, the replication is completed. Point the table to nextTable, and update sizeCtl to 0.75 times the size of the new array. The expansion is completed.

Get(K)

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    // Array initialized and not empty in specified bucket
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // Determine the head node first. If the hash value of the head node is the same as the hash value of the input parameter key
        if ((eh = e.hash) == h) {
            // The key of the header node is the key passed in
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val; 
        }
        // EH < 0 indicates that this node is a red black tree
        else if (eh < 0)
            // Search directly from the tree and return the result. If it doesn't exist, return null
            return (p = e.find(h, key)) != null ? p.val : null;
        // If the first node is not a lookup object and is not a red black tree structure, traverse the list
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    // No direct return null found
    return null;
}

Tags: Programming Java

Posted on Wed, 20 May 2020 07:08:03 -0400 by fellow21