Deep understanding of Java HashMap’s dead cycle

Time:2020-1-14

Preface

In the Taobao intranet, I saw a colleague posted that a CPU was 100% online broken, and this happened many times. The reason is that the Java language uses HashMap to create a race condition in the concurrent situation, which leads to a dead cycle. I also experienced this 4 or 5 years ago. I didn’t think it was easy to write. Because Java’s HashMap is non thread safe, there must be problems under concurrency. However, I found that in recent years, many people have experienced this event (you can see that many people are talking about it by checking “HashMap infinite loop” on the Internet). Therefore, I think this is a common problem. I need to write a vaccine article to talk about this event and show you how a perfect “race condition” is formed.

Symptoms of the problem

In the past, our java code used HashMap for some reasons, but at that time, the program was single threaded, and there was no problem. Later, there was a problem with our program’s performance, so we need to turn it into a multithreaded one. Then, after turning it into a multithreaded one, we found that the program often accounts for 100% of the CPU. Looking at the stack, you will find that the program hang in the HashMap. Get() method, and the problem disappears after restarting the program. But it will come again later. Moreover, this problem may be difficult to reproduce in the test environment.

We simply look at our own code, and we know that HashMap is operated by multiple threads. The Java document says that HashMap is non thread safe, and concurrent HashMap should be used.

But here we can study the reason.

Hash table data structure

I need to briefly talk about the classic data structure of HashMap.

HashMap usually uses an array of pointers (table []) to disperse all the keys. When a key is added, the index I of the array will be calculated by the hash algorithm through the key, and then the < key, value > will be inserted into the table [i]. If two different keys are counted in the same I, it is called conflict, also called collision, which will form a chain on the table [i] Table.

As we know, if the size of table [] is very small, for example, there are only two keys, if you want to put 10 keys, then the collision is very frequent, so an O (1) search algorithm becomes a chain table traversal, and the performance becomes o (n), which is the defect of hash table.

Therefore, the size and capacity of hash table are very important. Generally speaking, when the hash table container has data to insert, it will check whether the capacity exceeds the set threshold. If it exceeds the threshold, it needs to increase the size of the hash table, but in this way, all the elements in the hash table need to be recalculated. This is called rehash, which costs a lot.

I believe you are familiar with this basic knowledge.

Rehash source code of HashMap

Next, let’s take a look at the source code of Java’s HashMap.

Put a key and value into the hash table:

public V put(K key, V value)
{
......
// calculate Hash value
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
//If the key has been inserted, replace the old value (link operation)
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//The key does not exist. A node needs to be added
addEntry(hash, key, value, i);
return null;
}

Check whether the capacity exceeds the standard

void addEntry(int hash, K key, V value, int bucketIndex)
{
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//Check whether the current size exceeds the threshold set by us. If it exceeds the threshold, resize is required
if (size++ >= threshold)
resize(2 * table.length);
}

Create a larger hash table, and then migrate the data from the old hash table to the new hash table.

void resize(int newCapacity)
{
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
//Create a new hash table
Entry[] newTable = new Entry[newCapacity];
//Migrate data on old hash table to new hash table
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}

Note the highlight of the migrated source code:

void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
//The following code means:
//Take an element from the oldtable and put it in the newtable
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}

OK, this code is quite normal. And there’s no problem.

Normal rehash process

  • I assume that our hash algorithm simply uses key mod to calculate the size of the table (that is, the length of the array).
  • The top one is the old hash table, in which the size of hash table is 2, so key = 3, 7, 5, all conflict in table [1] after mod 2.
  • The next three steps are to resize hash table into 4, and then all the processes of < key, value > rehash

Rehash under concurrency

1) Suppose we have two threads.

Let’s go back to this detail in our transfer code:

do {
Entry < K, V > next = e.next; // < assuming that once the thread is executed, it will be suspended by scheduling
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);

And our thread two is finished.

Note that because e of thread1 points to key (3), and next points to key (7), it points to the link list after thread 2 rehash. We can see that the order of the linked list is reversed.

2) Once the thread is scheduled to execute.

First, execute newtalbe [i] = E;

Then E = next, which causes e to point to key (7),

The next loop’s next = e.next causes the next to point to the key (3)

3) All is well.

The threads work one after another. Remove the key (7), put it in the first one of the new table [i], and move E and next down.

The dead cycle of Java HashMap

4) A circular link appears.

e. Next = newtable [i] causes key (3). Next points to key (7)

Note: at this time, key (7). Next has pointed to key (3), and the circular list appears.

So, when our thread calls hashtable. Get (11), the tragedy is infinite loop.

Other

Someone reported the problem to sun, but Sun didn’t think it was a problem. Because HashMap does not support concurrency. To be concurrent, use concurrenthashmap

I’m here to record this, just to let you know and experience the danger of concurrent environment.

The above is the whole content of this article. I hope it will help you in your study, and I hope you can support developepaer more.