一个ConcurrentModificationException引发的血案

这个错误时比较常见的,今天我们源码分析下里边的机制,这个是我们在遍历list或map时经常会看到这样的错误java.util.ConcurrentModificationException:at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)

这个场景一般是我们是我循环遍历,根据某个条件来移除这个list或map中的对象

我们看下HashMap内部的实现 HashMap<K,V> 继承自 AbstractMap<K,V> ,AbstractMap<K,V>继承自Map<K,V>,看下Map里的实现

里边有一个静态的Entry<K,V> ,Entry可以理解为入口,接口里边定义了一个常规的方法;接口下面是一些Map常用的操作比如clear,remove,put等

AbstarctMap<K,V>继承自Map<K,V>


里边主要是封装了一些可能会共用到到方法,里边有Set<K>ketSet,可以理解为Key的集合,Collection<V>valuesCollection;value的集合,HashMap不就是Key-value嘛

HashMap继承自AbstractMap

public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable {
使用这个ConcurrentModificationException关键字在HashMap中搜索一下发现上面的解释

* <p>The {@code Iterator} created by calling the {@code iterator} method
* may throw a {@code ConcurrentModificationException} if the map is structurally
* changed while an iterator is used to iterate over the elements. Only the
* {@code remove} method that is provided by the iterator allows for removal of
* elements during iteration. It is not possible to guarantee that this
* mechanism works in all cases of unsynchronized concurrent modification. It
* should only be used for debugging purposes.
*
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
谷歌翻译

<p>通过调用{@code iterator}方法创建的{@code Iterator}
  *如果映射是结构化的,可以抛出{@code ConcurrentModificationException}
  *改变,而迭代器用于遍历元素。 只有
  * {@code remove}方法由迭代器提供允许删除
  *元素。 不可能保证这一点
  *机制适用于所有非同步并发修改的情况。 它
  *仅应用于调试目的。

意思很明了了,只有迭代器遍历时允许删除元素,其他方式如结构映射调用map.remove("key")等会抛出ConcurrentModificationException,这也是HashMap的Fail-Fast 快速失败机制,只允许迭代器这样处理,也许这也是迭代器的作用所在。

这个快速失败机制是怎么实现的呢,我们继续往下看

if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
if (nextEntry == null)
    throw new NoSuchElementException();
有两个变量,如果modCount != expectedModCount 则抛出这个异常,通过查找这个成员变量modCount 发现,不管map是remove,put,clear等modCount都会++,即modCount++,说明map操作数据了,数据被修改了,则次数增加;而expectedModCount 是指期待的操作次数,因为map.put 也会使modCount++,在HashMapIterator中有这样一段代码

private abstract class HashIterator {
    int nextIndex;
    HashMapEntry<K, V> nextEntry = entryForNullKey;
    HashMapEntry<K, V> lastEntryReturned;
    int expectedModCount = modCount;
给expectedModCount 赋值 则其默认值就是map.size();为什么说是在Iterator中遍历remove数据不回导致上述的异常,我们来看下代码

public void remove() {
    if (lastEntryReturned == null)
        throw new IllegalStateException();
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    HashMap.this.remove(lastEntryReturned.key);
    lastEntryReturned = null;
    expectedModCount = modCount;
}
这个remove方法是在HashIterator中实现的,迭代器每次remove都会将其重新赋值,因此两者恒等,不回出现上述问题,迭代器可以理解为数据库的游标,每次都会指向下一个元素,如果元素存在,则remove,我建立了一个测试类,验证上述我说的问题

public class JavaReleations {

    public static void main(String...params){
        Map<Integer,String> map = new HashMap<>();
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        map.put(1,"1");
        map.put(2,"2");
        map.put(3,"3");

        //我们比较常用的这个map循环移除
        for(Map.Entry<Integer,String> entry : map.entrySet()){
	System.out.println("map增强for循环:"+entry.getKey() + entry.getValue() + map.entrySet().size());
map.remove(1);//外部结构移除会造成ConcurrentModificationException异常 } //迭代器循环移除 Iterator<Map.Entry<Integer,String >> iterator = map.entrySet().iterator(); while (iterator.hasNext()){
	    System.out.println("迭代器循环:");
iterator.next(); iterator.remove();//迭代器内部移除不会出现ConcurrentModificationException } //List的内部机制和HashMap类似,也有类似于HashMap的快速失败机制// for (String str : list){//// list.remove(str);//// }
上述代码的注释已经说明问题,下面看下测试结果:

map增强for循环:113
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
at java.util.HashMap$EntryIterator.next(HashMap.java:1471)
at java.util.HashMap$EntryIterator.next(HashMap.java:1469)
at com.example.mrboudar.playboy.javarealations.JavaReleations.main(JavaReleations.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

Process finished with exit code 1

看到log发现只执行了一次循环,第二次时modCount值于expectedModCount已经不相等了,因而抛出ConcurrentModificationException异常,循环停止,也导致java虚拟机异常中断,下面的迭代器循环都没有执行。

注释掉map循环后,测试结果如下:

迭代器循环:
迭代器循环:
迭代器循环:


Process finished with exit code 0

发现没有抛出ConcurrentModificationException异常,java虚拟机正常结束。

上面验证了只有Iterator迭代器循环时是相对安全的,不会抛出异常,当然了,如果你还是想用map循环,或者需求只能通过那种方式来做,可以使用break,找到符合条件的及时break掉循环,防止下次循环抛出异常,或者建立temp来存储等方式

相对于Map,List的实现与之类似,也是用modCount 上面两个变量用来标示快速失败,循环移除也最好使用迭代器方式,或者for循环中及时break掉

好了,这个错误基本上是这样,大家要看多源码,多理解其中的实现,与君共勉吧!







  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值