一,情景引入
JRBM中有一个对球队Websocket在线情况的检测需求:
现有ConcurrentHashMap
现在有三个线程可能会同时对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中,就可以将最新的值也遍历到;