This is the 58th installment of the Java Programmer’s Way to Progress column, and we’ll talk about why HashMap is thread-unsafe.

01, multi-threaded capacity expansion will be an infinite loop

As we all know, HashMap resolves hash conflicts by using the zipper method, in which key-value pairs of the same hash value are stored in a linked list when a hash conflict occurs.

In JDK 7, lists were stored in the header insertion mode, meaning that the next conflicting key/value pair is placed before the previous one (new elements in the same position are placed at the head of the list). The expansion may lead to circular linked lists, resulting in an infinite loop.

Resize method source code:

// newCapacity indicates the newCapacity
void resize(int newCapacity) {
    // Small array, temporary over
    Entry[] oldTable = table;
    // Capacity before capacity expansion
    int oldCapacity = oldTable.length;
    // MAXIMUM_CAPACITY is the maximum capacity, 2 ^ 30 = 1<<30
    if (oldCapacity == MAXIMUM_CAPACITY) {
        // Adjust the capacity to Integer's maximum value 0x7FFFFFFf (hexadecimal) =2 to the 31th power -1
        threshold = Integer.MAX_VALUE;
        return;
    }

    // Initialize a new array.
    Entry[] newTable = new Entry[newCapacity];
    // Move elements from a small array to a large array
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // Reference the new large array
    table = newTable;
    // Recalculate the threshold
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
Copy the code

The transfer method is used to transfer the elements of a small array into a new array.

void transfer(Entry[] newTable, boolean rehash) {
    // New capacity
    int newCapacity = newTable.length;
    // Iterate over the small array
    for (Entry<K,V> e : table) {
        while(null! = e) {// Different values on the same key
            Entry<K,V> next = e.next;
            // Whether to recalculate the hash
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            // Calculate the index of the element in the array based on the size of the array and the hash of the key
            int i = indexFor(e.hash, newCapacity);

            // New elements in the same position are placed at the head of the list
            e.next = newTable[i];

            // Put it on the new array
            newTable[i] = e;

            // The next element on the liste = next; }}}Copy the code

Note that the lines e.ext = newTable[I] and newTable[I] = e place the new element at the same position at the head of the list.

Let’s say it looks like this before expansion.

So the normal expansion would look something like this.

Assume that two threads are performing capacity expansion at the same time, and thread A is executing to newTable[I] = e; Thread A: e=3, next=7, e.next=null

Thread B starts execution and completes the data transfer.

At this point, next of 7 is 3 and next of 3 is null.

Then thread A gets the CPU time slice and continues to execute newTable[I] = e, putting 3 into the corresponding position of the new array. After executing this loop, thread A’s situation is as follows:

Execute the next loop, at this point e=7, the next of 7 in thread A was originally 5, but since the table is shared by thread A and thread B, and thread B completes smoothly, the next of 7 becomes 3, so the next of 7 in thread A is also 3.

With head insertion, it looks like this:

Ok, so next = 3, e = 3.

Proceed to the next loop, but this should be the last loop since thread B has null for next at 3.

Next =7; next=7; next=7; next=7;

When the dolls begin, element 5 becomes an abandoned baby

However, this issue was fixed in JDK 8 and the list will remain in the original order for expansion, as described in the HashMap expansion mechanism article.

02. Put causes elements to be lost in multiple threads

Normally, when a hash conflict occurs, a HashMap looks like this:

However, when multiple threads perform the PUT operation at the same time, if the calculated index positions are the same, the previous key will be overwritten by the next key, resulting in element loss.

Put:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;

    // Step 1: If TAB is empty, create one
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    // Step 2: calculate index and handle null
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;

        // Step 3: The node key exists, overwriting the value directly
        if(p.hash == hash && ((k = p.key) == key || (key ! =null && key.equals(k))))
            e = p;

        // Step 4: Determine that the chain is a red-black tree
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

        // Step 5: The chain is a linked list
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);

                    // List length greater than 8 is converted to red black tree for processing
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }

                // Key already exists overwriting value directly
                if(e.hash == hash && ((k = e.key) == key || (key ! =null && key.equals(k))))
                    break; p = e; }}// Step 6
        if(e ! =null) { // existing mapping for key
            V oldValue = e.value;
            if(! onlyIfAbsent || oldValue ==null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;

    // Step ⑦ : Expand the capacity when the capacity exceeds the maximum
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
Copy the code

The problem occurs in step ② :

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
Copy the code

If thread A executes TAB [I] = newNode(hash, key, value, null), thread A executes TAB [I] = newNode(hash, key, value, null)

Thread B then executes TAB [I] = newNode(hash, key, value, null).

Three gets killed.

03. Concurrent PUT and GET causes GET to null

When thread A performs PUT, the number of elements increases because the number exceeds the threshold. When thread B performs GET, this problem may occur.

Resize:

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) {
        // If it exceeds the maximum value, it will no longer be expanded
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // If the value is not higher than the maximum value, the value is doubled
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // Calculate the new resize upper limit
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
}
Copy the code

Table = newTab; table = newTab; table = newTab;


In order to facilitate you to learn Java more systematically, the second brother has “Java Programmer progress” column open source to GitHub, you just need to gently star, you can fight strange upgrades with all your friends.

GitHub address: github.com/itwanger/to…