[summary] Java set (Map)

Map

01 / HashMap

In JDK8, the bottom layer of HashMap is realized by "array + linked list + red black tree". When learning the principle of HashMap, we should focus on its data structure, put process and capacity expansion mechanism.

Source code interpretation:

Data structure:

At the beginning, it must be array + linked list, and when it reaches a certain scale, it will be transformed into a tree.

The tree node inherits the linked list node and retains the characteristics of the linked list when the data structure is a tree. It is very easy to change from a tree to a linked list.

transient Node<K,V>[] table;//Array of Node type (each Node in the array can be a one-way linked list)
//Node, single linked list
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    . . . 
}
//Tree structure (TreeNode indirectly inherits Node)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
}

1.1 process of put()

1. If the array is empty, it will be expanded for the first time.

2. If the head node at the ith position of the array is empty (the linked list does not exist), a new linked list node will be created.

3. If the head node at the ith position of the array is not empty (a linked list or tree already exists), insert the element into the slot.

  • If the element is equal to the header node (key), it is directly overwritten;
  • If the element is a tree node, it is added to the tree;
  • If the element is a linked list node and the length of the linked list is less than 8, it will be added to the linked list
  • If the element is a linked list node and the length of the linked list reaches 8, expand the capacity or turn it into a tree, and then add the element;

4. If the number of elements exceeds the threshold, the capacity will be expanded again.

Source code interpretation:

public V put(K key, V value) {
    //First calculate the hash value of the key and take the module of the array as the subscript
        return putVal(hash(key), key, value, false, true);
    }
//DP Hash 
static final int hash(Object key) {
        int h;
    //Take the hashCode() of the key first, and then move the result of hashCode on XOR to the right by 16 bits
    //Do XOR operation between high and low to reduce the probability of collision
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
     //1. Array is empty
        if ((tab = table) == null || (n = tab.length) == 0)
            //Initial expansion
            n = (tab = resize()).length;
     //Calculate the position of the element (hash function and array length and operation), and obtain when the header node is empty
        if ((p = tab[i = (n - 1) & hash]) == null)
            //Create linked list node
            tab[i] = newNode(hash, key, value, null);
        else {//When neither array nor header node is empty
            Node<K,V> e; K k;
            //If the passed in key and the header node are equal
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //cover
                e = p;
            else if (p instanceof TreeNode)//If the head node is a tree
                //Add elements to the tree
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//It is neither a head node nor a tree, that is, a linked list
                for (int binCount = 0; ; ++binCount) {
                    //If the subsequent node bit of the head node is null
                    if ((e = p.next) == null) {
                        //Create a new node and insert it behind the linked list
                        p.next = newNode(hash, key, value, null);
                        //After inserting the data, check whether the length is greater than or equal to 8-1
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //Convert to tree (judge whether to expand or convert to tree)
                            treeifyBin(tab, hash);
                        break;
                    }
                    //During traversal, it is found that an element is equal to the current element (i.e. a node already exists)
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //The value of the processing node. The above is the location of the processing node
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //Allow updating existing values
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//Update the value of the node
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;//Increase modification quantity
        if (++size > threshold)//Is the array size greater than the threshold
            resize();//Capacity expansion
        afterNodeInsertion(evict);
        return null;
    }

When the length of the linked list reaches 8, first judge whether the length of the array is greater than 64. If it is greater than 64, it will be transformed into a tree. If it is less than 64, expand the capacity first

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //Determine array length and 64 size
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();//Capacity expansion
    //Convert linked list to tree
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

hashCode(): used to determine where to put it

static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;

1.2 capacity expansion mechanism

The default capacity is 16. Double the capacity every time you expand the capacity. The capacity must be to the nth power of 2
16 = 00010000, 32 = 00100000
hash & (16-1) = hash & 00001111
hash & (32-1) = hash & 00011111
After doubling and capacity expansion, the calculated index value is one more bit,
If this bit is 0, the position remains unchanged, otherwise the position is + 16.

What about moving the data location? Recalculate? no The node is either in the original slot or in the position of the original slot + 16 (the first 16 low-level slots and the last 16 high-level slots)

Source code interpretation:

 int threshold;  //Artificially specified capacity
final float loadFactor; //The default value of load factor is 0.75

//constructor 
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

//The default value of load factor is 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//Constructor with specified capacity
 public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
//
 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor; //Initialize load factor
        this.threshold = tableSizeFor(initialCapacity);//Initialize capacity for processing
    }

//Convert the artificially specified capacity calculation to the nth power of close to 2
static final int tableSizeFor(int cap) {
        int n = cap - 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;
    }

Expansion source code:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        //Determine whether the old capacity exceeds the maximum
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //When the maximum capacity is not reached, the new capacity is twice the old capacity
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //Old threshold
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        //Give a default capacity of 16
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {//When capacity bit 0 is specified
        //critical value
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //Reinitialize the new array. The above is the double expansion mechanism, and the following is the migration mechanism
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;//Assign a new array
    //The previous array is not empty. Traverse the old bucket
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;//Empty old tub
                if (e.next == null)//When the head node of the current slot has no successor
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)//If the head node is a tree
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//Add to tree
                else { // preserve order is not a tree, it is a linked list
                    //Head and tail nodes of low-level slot
                    Node<K,V> loHead = null, loTail = null;
                    //Head and tail nodes of high-level slot
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    //The position of the distribution slot, in the low-level slot or in the high-level slot
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {//Low level slot
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {//High level slot
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //Locate the position in front and start moving the data below
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;//Put the low position in the original position
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;//Place the high position at the original + doubled capacity position
                    }
                }
            }
        }
    }
    return newTab;
}

Tree migration in migration mechanism

It is also to allocate node locations first, and then link nodes to the tree

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
    //Low high slot node
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
    //Ergodic iteration
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                //Next is a one-way linked list pointing to the next node, not a red black tree. The tree is iterated as a linked list (although it is a tree, the pointing relationship of the linked list remains)
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {//Low non empty
                //The length of the linked list is less than or equal to 6
                if (lc <= UNTREEIFY_THRESHOLD)
                    //Convert tree to linked list
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {//High non empty
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }
static final int UNTREEIFY_THRESHOLD = 6;//If the length of the linked list is less than or equal to 6, turn the tree into a linked list

When will red and black trees degenerate into linked lists?

During capacity expansion, when a tree in the old array is moved to the new array, the length of the original red and black tree is less than or equal to 6 after it is disassembled (part in the low position and part in the high position), and it will degenerate into a linked list

1.3 query mechanism

1. When querying, locate the slot first, and then iterate the linked list or tree from the slot to find the corresponding node;
2. During iteration, first traverse the slot, then traverse the linked list or tree in the slot, and traverse all nodes.

Source code interpretation:

get

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

//
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //Find the position of the head node according to the key
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {//Get header node

            if (first.hash == hash && // always check first node
                //Judge whether the key you pass in is equal to the key of the header node
                ((k = first.key) == key || (key != null && key.equals(k))))
                //Equal returns the header node
                return first;
            //Unequal iteration
            if ((e = first.next) != null) {
                //Iterative linked list
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

Iteration key

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

//
 final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
     //iterator 
        public final Iterator<K> iterator()     { return new KeyIterator(); }
       . . . . . . 
        
    }

//Concrete iterator
final class KeyIterator extends HashIterator//inherit
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

//Parent iterator
abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            //Traverse the slot from scratch
            if (t != null && size > 0) { // advance to first entry
                //First non empty slot found
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

    //Find next node
        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        . . . . . 
    }

1.4 relevant parameters

It doesn't mean that if the length of the linked list reaches 8, it will be transformed into a tree. It also depends on whether the length of the array reaches 64

02 / TreeMap

The bottom layer of TreeMap is implemented by red black tree structure. When inserting elements, it needs to be compared. TreeMap allows elements to be compared through the default comparator or the specified comparator. These two comparison methods are called natural sorting and custom sorting respectively. Data structure and sorting rules are the key contents to be paid attention to when learning TreeMap.

Source code interpretation:

 private transient Entry<K,V> root;//Root node Entry of the tree

// Entry structure red black tree
static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;//Identification of color
}

comparator

 private final Comparator<? super K> comparator;

//The default constructor initializes the comparator to null, so that the method provided by the element can be sorted naturally
public TreeMap() {
        comparator = null;
    }

//Pass in the constructor you specified to customize the sorting
 public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

//The key of the comparison rule TreeMap must implement the Comparable interface!!!
final int compare(Object k1, Object k2) {
    //If the comparator is empty, the two key s are compared. If it is not empty, custom sorting is used
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
    }

put

public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        //Purpose to use this method to check the key. The key may be null or does not implement the Comparable interface
        compare(key, key); // type (and possibly null) check

        //When the root node is empty, initialize the root node first
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {//Comparator is not empty, custom sorting
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {//Natural sorting
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);//Comparison using custom comparator
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    //Add data
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

01 / sync collection

The Collections class provides multiple synchronizexXxx() methods, which can wrap the specified collection into a thread synchronized collection, so as to solve the thread safety problem when multiple threads access the collection concurrently.

Collections is a tool class and Collection is an interface

public static <T> Collection<T> synchronizedCollection(Collection<T> c) {
    return new SynchronizedCollection<>(c);
}

 static class SynchronizedCollection<E> implements Collection<E>, Serializable {
        private static final long serialVersionUID = 3053995032091335093L;

        final Collection<E> c;  // Backing Collection
        final Object mutex;     // Object on which to synchronize

        SynchronizedCollection(Collection<E> c) {
            this.c = Objects.requireNonNull(c);
            mutex = this;
        }

        SynchronizedCollection(Collection<E> c, Object mutex) {
            this.c = Objects.requireNonNull(c);
            this.mutex = Objects.requireNonNull(mutex);
        }
     
       public int size() {
            synchronized (mutex) {return c.size();}
        }
. . . . . 
 }

Note: the Collection does not lock all methods. The iterative method does not lock and needs to lock itself

Summary: use the synchronizexXxx() method of Collections to turn a Collection into a thread safe Collection. Almost all methods will be locked, but the iterative methods of Collection are not locked

02 / invariant set

Collections provides the following three types of methods to return an immutable Collection. The parameters of these three types of methods are the original Collection object, and the return value is the "read-only" version of the Collection. Through the three types of methods provided by collections, a "read-only" Collection or Map can be generated.

  • emptyxxx(): returns an empty immutable collection object;
  • singletonx): returns an immutable collection object containing only the specified object;
  • Unmodifiablexxxxx(): returns the immutable view of the specified collection object.

Immutable. It is thread safe at this time, and it can also solve the problem of concurrent thread safety

Collection under JUC package

brief introduction

01 / CopyOnWriteArrayList

As its name implies, CopyOnWriteArrayList implements write operations by copying the underlying array. When a thread performs a read operation on the CopyOnWriteArrayList collection, the thread will directly read the collection itself without locking and blocking.
When a thread writes to the copyonwritearraylist collection, the collection will copy a new array at the bottom, and then write to the new array. Since the copyonwritearraylist collection writes to a copy of the array, it is line safe.
It should be noted that CopyOnWriteArrayList has poor performance because it needs to copy arrays frequently when performing write operations. However, since the read operation and write operation are not the same array, and the read operation does not need to be locked, the read operation is fast and safe. Therefore, CopyOnWriteArrayList is suitable for scenarios where the read operation is much larger than the write operation , such as caching, etc.

Source code interpretation:

final transient ReentrantLock lock = new ReentrantLock();//With reentrant lock
private transient volatile Object[] array;//The bottom layer is an array

//default constructor 
 public CopyOnWriteArrayList() {
        setArray(new Object[0]);//Initialization array length is 0
    }
//Pass in an array. Now copy the array
public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

The get read method is unlocked

public E get(int index) {
    return get(getArray(), index);
}
 private E get(Object[] a, int index) {
        return (E) a[index];
    }

Modify operation (lock when copying array)

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        //Copy array
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //Replace original array
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

Iterative method

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

static final class COWIterator<E> implements ListIterator<E> {
        /** Snapshot of the array */
        private final Object[] snapshot;
        /** Index of element to be returned by subsequent call to next.  */
        private int cursor;

    //Assign the original array to snapshot (iterative copy)
        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }

Summary: CopyOnWriteArrayList is not mutually exclusive in reading and writing (solved through the copy array), but is mutually exclusive in writing (because a lock is added to the writing)

02 / ConcurrentHashMap

Implementation method in JDK 7:
In order to improve concurrency, in JDK 7, a HashMap is divided into multiple sub hashmaps. Each sub HashMap is called a Segment (reducing lock granularity), and multiple threads operate multiple segments independently of each other. The segmentation lock in JDK 7 has three advantages:

1. Reduce Hash conflicts and avoid too many elements in a slot.

2. Improve the concurrency of reading and writing, and segments are independent of each other.

3. The concurrency of capacity expansion is improved. Instead of expanding the entire ConcurrentHashMap, each Segment is expanded independently.

Each slot stores segments, and each segment stores a HashMap.

Summary: lock in sections

Implementation method in JDK 8:
There are great changes in the implementation of JDK 8. First, there is no segment lock, and all data is placed in a large HashMap. Second, a red black tree is introduced. If the head Node is a Node type, it is followed by an ordinary linked list. If the head Node is a TreeNode type, its back is a red black tree, and TreeNode is a subclass of Node. The advantages of this implementation are:

1. Using the red black tree, when there are many elements in a slot, its query and update speed will be much faster than the linked list, and the problem of Hash conflict can be better solved.
2. The granularity of locking is not the entire ConcurrentHashMap, but each head Node is locked separately, that is, the degree of concurrency, that is, the length of the Node array. The initial length is 16, which is the same as the number of initial segments in JDK 7.

3. Concurrent capacity expansion is the most difficult. In JDK7, once the number of - segments is established during initialization, it cannot be changed, and the concurrency is fixed. After that, it is only expanded within each Segment, which means that each Segment is expanded independently, does not affect each other, and there is no problem of concurrent capacity expansion. However, in jdk8, it is equivalent to only one Segment, when a thread wants to expand a Node Array, other threads have to read and write, so the processing process is very complex.

Node (linked list) and TreeNode (tree) can be stored in each slot

Source code interpretation:

//Two table s
transient volatile Node<K,V>[] table;//An array of storage elements
 private transient volatile Node<K,V>[] nextTable;//table used during capacity expansion

private transient volatile int sizeCtl;//Control the initialization and capacity expansion mechanism

//constructor 
 public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;//Default capacity 16
        putAll(m);
    }
//The constructor of the specified capacity converts the specified number to the nth power close to 2
 public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));//Expand capacity to 1.5x
        this.sizeCtl = cap;
    }

//
 public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
     //
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
     //Initial capacity < concurrency level (number of concurrent threads, computer cores)
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            //Increase the specified capacity to the number of computer cores
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

put

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    //Iterative array
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //The array is empty
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();//Initialize array
        //If the head node at position i is empty
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //Create a new node and save it
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //If the header node is not empty: judge that the hsah value of the header node is - 1 (migrating to a new array during capacity expansion)
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);//To help with capacity expansion and data migration
        else {//The header node is not empty and is not migrating: data can be saved
            V oldVal = null;
            synchronized (f) {//Lock the head node!!!!
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {//hash of the head Node = 0 (ordinary Node, linked list, add the number to the linked list)
                        . . . . 
                            }
                        }
                    }
                    //The node is a tree. Add the number to the tree
                    else if (f instanceof TreeBin) {
                        . . . 
                        }
                    }
                }
            }
            //After adding the number, judge whether the length of the linked list has reached the threshold
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //Judge whether the element length of the array has reached the threshold (3 / 4)
    addCount(1L, binCount);
    return null;
}

Points that may cause capacity expansion during put:

1. tab = helpTransfer(tab, f); / / help with capacity expansion and data migration

2. treeifyBin(tab, i); / / when converting to a tree, first look at the array capacity. Do you want to expand it

3. addCount(1L, binCount); / / judge whether the element length of the array reaches the threshold (3 / 4)

addCount

private final void addCount(long x, int check) {
  . . . . . 
                transfer(tab, null);//Real capacity expansion
            s = sumCount();
        }
    }
}

Real capacity expansion mechanism

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    //Step size, calculate how many nodes each cpu processes
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    //Construct a new array before migration
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            //The capacity of the new array is 2x
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
    //Forwarding node. When querying the old array, save the node corresponding to the new array
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    
    for (int i = 0, bound = 0;;) {//bound is the boundary
        . . . 
            //Grab the task in the way of CAS
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        //The whole collection is traversed
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {//Transfer complete
                nextTable = null;
                table = nextTab;//Replace the old array
                sizeCtl = (n << 1) - (n >>> 1);//Calculate again as the threshold for the next expansion
                return;
            }
            . . . . 
        }
        //The ith location is empty (the migration has been completed)
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);//Set the old array as the forwarding node
        else if ((fh = f.hash) == MOVED)//Migrating
            advance = true; // already processed
        else {
            synchronized (f) {//Lock the head node and move the number of plays
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {//Description is a linked list, copy several plays
                        . . . . 
                    }
                    else if (f instanceof TreeBin) {//It means that the red and black tree plays
                        . . . 
                    }
                }
            }
        }
    }
}

Initialize array

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    //If the array is empty, initialize
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)//Determine whether other threads are initializing the array
            Thread.yield(); // lost initialization race; just spin, the thread makes a concession and turns to the ready state
        //Initialize the array as CAS
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    //Array initialized to or default
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    //Change, 3 / 4 of the original value, the key node for the next expansion
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

Summary of ConcurrentHashMap mechanism:

  • Initial operation: initialize the array and header node in CAS mode;
  • Insert node: lock when inserting a node at a certain location;
  • Capacity expansion: each thread is responsible for expanding part of the data, and locking is performed during capacity expansion. In addition, read and write operations are still supported during capacity expansion. If the node at this location is not fwd, read and write directly. Otherwise, access the new array for reading and writing.

Tags: Java data structure linked list

Posted by prent327 on Wed, 22 Sep 2021 03:33:42 +0530