HashMap、ConcurrentHashMap单线程、多线程遍历时修改的异同

一,情景引入

JRBM中有一个对球队Websocket在线情况的检测需求:

现有ConcurrentHashMap map,jrbmSession包括了session和lastAliveTime,前端每隔3s通过ws连接向服务端发送一次心跳,服务端接收到心跳之后,更新对应JrbmSession中的lastAliveTime。

现在有三个线程可能会同时对map进行操作:

线程A:每隔3s遍历一次map.values(),判断lastAliveTime到现在是否已经超过3s了,如果超过3s,则说明心跳异常,用map.remove()将其踢下线

线程B:接收前端发来的请求,如果是连接关闭请求,那么就map.remove()

线程C:接收新的连接,map.put()

简单来说,有可能在同一时间,线程A正在遍历map,线程B需要删除一个元素,线程C需要添加一个元素,这样会抛出ConcurrentModificationException吗?

二,实验说明

对于一个map,有以下几种情况:

map:HashMap、ConcurrentHashMap

线程:单线程、多线程

遍历:for、foreach、iterator

操作:map.put、map.remove、iterator.remove

ps:这里插一句,map.keySet().iterator、map.values().iterator、map.entrySet().iterator,这三个iterator的remove方法都能直接删除map中的元素,而不是删除拿出来的set的元素,直接看代码:

    final class KeySet extends AbstractSet {
        public final Iterator iterator()     { return new KeyIterator(); }
    }
    final class Values extends AbstractCollection {
        public final Iterator iterator()     { return new ValueIterator(); }
    }
    final class EntrySet extends AbstractSet> {
        public final Iterator> iterator() {return new EntryIterator();}
    }

可以看到,他们都返回了相应的Iterator对象,那么这些Iterator对象在哪里定义的呢? 

    abstract class HashIterator {
        public final void remove() {
            Node 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 {
        public final K next() { return nextNode().key; }
    }

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

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

这几个Iterator都继承了HashIterator,并且在next()方法中返回了HashIterator中节点的相应键或值,也就是说,这几个Iterator实际上调用的还是操作map的那个Iterator的方法,因此并不是单独对某个keySet或valuesSet进行操作。

笛卡尔积为24种情况,其实有些情况是类似的,比如foreach底层就是iterator,但是foreach不能使用iterator的remove方法,因此所有的遍历我都用iterator,最终的测试方案简化为以下几种:

1. HashMap-单线程-iterator-map.put(ConcurrentModificationException)

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

在第二次执行iterator1.next()时,这时候map中的modcount已经和iterator中的expectedModCount不一致了,所以报错,代码如下:

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

2. HashMap-单线程-iterator-map.remove(ConcurrentModificationException)

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

理由同1

3. HashMap-单线程-iterator-iterator.remove(Success)

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

这次使用iterator自带的remove方法,最后发现每个元素都被正常的移除了,这是因为在iterator.remove()中,会将移除后map的modCount重新赋值给自身的expectedModCount,这样下次就不会抛异常了,代码如下:

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

ps:HashMap本身就是线程不安全的,因此多线程情况下的实验就不用做了。

4. ConcurrentHashMap-单线程-iterator-map.put(Success)

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


输出:

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

不仅发现没有报错,而且遍历时还把新加入的元素给遍历到了,之所以没报错是因为CHM中的iterator没有了modCount属性,并且每次继续遍历的时候,都会去从最新的map中获取下一个元素接到当前元素之后,以此达到能遍历到新增元素的目的。

5. ConcurrentHashMap-单线程-iterator-map.remove(Success)

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


输出:

1
2
{1:1,2:2}

同样我们看到没有报错,并且也没有把3给遍历出来,但这并不代表每一个被删除的数都能不被遍历,如果我把2给移除,会发现2仍然被遍历到了

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


输出:

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

这是因为,在删除2之前,2已经被加载到next的值中了,所以照样会遍历到2.

6. ConcurrentHashMap-单线程-iterator-iterator.remove(Success)

不用测了,肯定ok

7. ConcurrentHashMap-多线程-iterator-map.put(Success)

        Map 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 iterator = map.values().iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
            Thread.sleep(20);
        }
        thread.join();
        System.out.println(JSON.toJSONString(map));

输出:

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}

解释一下这段程序,主线程负责遍历,thread线程负责添加元素,可以看到没有报错并且每个元素都被打印出来了,毕竟是加了锁的。

8. ConcurrentHashMap-多线程-iterator-map.remove(Success)

        Map 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 iterator = map.values().iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
            Thread.sleep(20);
        }
        thread.join();
        System.out.println(JSON.toJSONString(map));

输出:

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

主线程负责遍历,thread线程删除3-5,这里我为什么把thread线程中的sleep时间缩短了呢?如果还是sleep(20),主线程中iterator已经把要删除的元素加载到next中了,那么仍然会打印出来,所以必须要在next加载之前,把元素删除。

 

三,结论

HashMap等一众非线程安全的容器,是不支持遍历时添加或删除的,但是可以通过iterator.remove()方法来安全的删除当前元素;

ConcurrentHashMap则支持遍历时添加或删除,并且只要所删除的元素还没被加载到next中,就可以将最新的值也遍历到;

你可能感兴趣的:(JRBM)