The similarities and differences of HashMap and ConcurrentHashMap modification in single thread and multi thread traversal

Keywords: JSON Session Attribute

1, Scenario introduction

JRBM has a demand for detecting the online situation of team Websocket:

The existing concurrenthashmap < teamid, jrbmsession > map, jrbmsession includes session and lastlivetime. The front end sends heartbeat to the service end through ws connection every 3s. After receiving the heartbeat, the service end updates the lastlivetime in the corresponding jrbmsession.

Now there are three threads that may operate on the map at the same time:

Thread A: traverse map.values() every 3s to determine whether lastlivetime has exceeded 3s. If it exceeds 3s, the heartbeat is abnormal. Use map.remove() to kick it off the line

Thread B: receives the request from the front end. If the connection is closed, map.remove()

Thread C: receive new connection, map.put()

In short, it is possible that at the same time, thread A is traversing the map, thread B needs to delete an element, and thread C needs to add an element. Will this throw A ConcurrentModificationException?

2, Experimental description

For a map, there are several situations:

map:HashMap,ConcurrentHashMap

Thread: single thread, multi thread

Traversal: for, foreach, iterator

Operations: map.put, map.remove, iterator.remove

ps: insert a sentence here. map.keySet().iterator, map.values().iterator, map.entrySet().iterator. The remove method of these three iterators can directly delete the elements in the map, rather than the set elements. See the code directly:

    final class KeySet extends AbstractSet<K> {
        public final Iterator<K> iterator()     { return new KeyIterator(); }
    }
    final class Values extends AbstractCollection<V> {
        public final Iterator<V> iterator()     { return new ValueIterator(); }
    }
    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final Iterator<Map.Entry<K,V>> iterator() {return new EntryIterator();}
    }

As you can see, they all return corresponding Iterator objects, so where are these Iterator objects defined?  

    abstract class HashIterator {
        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }  
    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

    final class ValueIterator extends HashIterator
        implements Iterator<V> {
        public final V next() { return nextNode().value; }
    }

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

These iterators inherit the HashIterator and return the corresponding key or value of the node in the HashIterator in the next() method. That is to say, these iterators actually call the Iterator method that operates the map, so they do not operate on a keySet or valuesSet alone.

There are 24 cases of Cartesian product. In fact, some cases are similar. For example, the bottom layer of foreach is iterator, but foreach can't use the remove method of iterator, so I use iterator for all iterations. The final test scheme is simplified as follows:

1. HashMap single thread iterator map.put (ConcurrentModificationException)

        Map<Integer,Integer> map=new HashMap<>();
        map.put(1,1);
        map.put(2,2);
        map.put(3,3);
        Iterator<Integer> iterator = map.values().iterator();
        while(iterator.hasNext()){
            iterator.next();
            map.put(4,4);
        }
        System.out.println(JSON.toJSONString(map));

When executing iterator1.next() for the second time, the modcount in the map is inconsistent with the expectedModCount in the iterator, so an error is reported. The code is as follows:

if (modCount != expectedModCount)
       throw new ConcurrentModificationException();

2. HashMap single thread iterator map.remove (ConcurrentModificationException)

        Map<Integer,Integer> map=new HashMap<>();
        map.put(1,1);
        map.put(2,2);
        map.put(3,3);
        Iterator<Integer> iterator = map.values().iterator();
        while(iterator.hasNext()){
            iterator.next();
            map.remove(3);
        }
        System.out.println(JSON.toJSONString(map));

The same reason 1

3. HashMap single thread iterator iterator.remove (Success)

        Map<Integer,Integer> map=new HashMap<>();
        map.put(1,1);
        map.put(2,2);
        map.put(3,3);
        Iterator<Integer> iterator = map.values().iterator();
        while(iterator.hasNext()){
            iterator.next();
            iterator.remove();
        }
        System.out.println(JSON.toJSONString(map));

This time, we use the remove method provided by iterator. At last, we find that every element has been removed normally. This is because in iterator.remove(), the map's modCount will be reassigned to its expectedModCount after removal, so that next time we will not throw exceptions. The code is as follows:

        public final void remove() {
            ...
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }

ps: HashMap itself is thread unsafe, so experiments in the case of multithreading need not be done.

4. ConcurrentHashMap single thread iterator map.put (Success)

        Map<Integer,Integer> map=new ConcurrentHashMap<>();
        map.put(1,1);
        map.put(2,2);
        map.put(3,3);
        Iterator<Integer> iterator = map.values().iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
            map.put(4,4);
        }
        System.out.println(JSON.toJSONString(map));


//Output:

1
2
3
4
{1:1,2:2,3:3,4:4}

It is not only found that there is no error reported, but also the newly added elements are traversed. The reason why there is no error reported is that the iterator in CHM has no modCount attribute, and every time the traversal continues, it will get the next element from the latest map to receive the current element, so as to achieve the purpose of traversing to the new element.

5. ConcurrentHashMap single thread iterator map.remove (Success)

        Map<Integer,Integer> map=new ConcurrentHashMap<>();
        map.put(1,1);
        map.put(2,2);
        map.put(3,3);
        Iterator<Integer> iterator = map.values().iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
            map.remove(3);
        }
        System.out.println(JSON.toJSONString(map));


//Output:

1
2
{1:1,2:2}

In the same way, we see that no error is reported and no 3 is traversed, but this does not mean that every deleted number can not be traversed. If I remove 2, I will find that 2 is still traversed

        Map<Integer,Integer> map=new ConcurrentHashMap<>();
        map.put(1,1);
        map.put(2,2);
        map.put(3,3);
        Iterator<Integer> iterator = map.values().iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
            map.remove(2);
        }
        System.out.println(JSON.toJSONString(map));


//Output:

1
2
3
{1:1,3:3}

This is because, before deleting 2, 2 has been loaded into the next value, so it will still traverse to 2

6. ConcurrentHashMap - single thread - iterator-iterator.remove (Success)

You don't have to test it. It must be ok

7. ConcurrentHashMap - multithread - iterator map.put (Success)

        Map<Integer,Integer> map=new ConcurrentHashMap<>();
        map.put(1,1);
        map.put(2,2);
        map.put(3,3);
        Thread thread = new Thread(() -> {
            int i = 4;
            while (i < 10) {
                try {
                    map.put(i, i);
                    i++;
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        Iterator<Integer> iterator = map.values().iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
            Thread.sleep(20);
        }
        thread.join();
        System.out.println(JSON.toJSONString(map));

//Output:

1
2
3
4
5
6
7
8
9
{1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9}

Explain this program. The main thread is responsible for traversal and the thread thread is responsible for adding elements. You can see that there is no error and each element is printed out. After all, it is locked.

8. ConcurrentHashMap - multithread - iterator map. Remove (Success)

        Map<Integer,Integer> map=new ConcurrentHashMap<>();
        map.put(1,1);
        map.put(2,2);
        map.put(3,3);
        map.put(4,4);
        map.put(5,5);
        map.put(6,6);
        Thread thread = new Thread(() -> {
            int i = 3;
            while (i < 6) {
                try {
                    map.remove(i);
                    i++;
                    Thread.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        Iterator<Integer> iterator = map.values().iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
            Thread.sleep(20);
        }
        thread.join();
        System.out.println(JSON.toJSONString(map));

//Output:

1
2
6
{1:1,2:2,6:6}

The main thread is responsible for traversal, and the thread thread deletes 3-5. Why do I shorten the sleep time in the thread thread thread? If it's still sleep(20), the iterator in the main thread has loaded the element to be deleted into next, it will still print out, so you must delete the element before next loading.

 

Three, conclusion

A number of non thread safe containers, such as HashMap, do not support adding or deleting during traversal, but you can safely delete the current element through the iterator.remove() method;

ConcurrentHashMap supports adding or deleting during traversal, and as long as the deleted element has not been loaded into next, you can also traverse the latest value to;

Published 124 original articles, won praise and 10000 visitors+
Private letter follow

Posted by HaXoRL33T on Thu, 06 Feb 2020 04:28:58 -0800