【解决】让我们会一会 ConcurrentModificationException

巧避ConcurrentModificationException
本文通过一个具体的案例,详细解析了ConcurrentModificationException产生的原因及其解决方案,包括使用迭代器和增强for循环时如何避免该异常。

我们在敲代码的过程中,难免会和一些Exception不期而遇,就像不知什么时候就会触碰到的女朋友的小脾气,并且,Exception也正像女朋友一样,你需要花上一些时间去了解对方,才知道自己是怎么“死”的。和ConcurrentModificationException的初相遇,那是在一个秋末夜晚,昏黄的路灯下……好了,不扯了。

现在,我们来会一会而ConcurrentModificationException。问题出现在做一个模拟影院自主购票的控制台小项目的过程中,需要一个方法来将选中的电影加入到购物车列表。具体如下。


	Iterator<MovieInCart> it = chosenList.iterator();				
	if(it.hasNext()==false){			    //购物车列表为空
		chosenList.add(movieInCart);<span style="white-space:pre">	</span>            //直接将该电影加入购物车列表
	}else{
		while(it.hasNext()){			     //判断购物车列表里是否已经有该电影
			<strong>MovieInCart movieExisted = it.next();//该语句报错 ConcurrentModificationException</strong>
			if(movieInCart.equals(movieExisted)){//有的话 仅更改已存在的该电影的购买数量
				movieExisted.setQuantity(movieExisted.getQuantity()+quantity);
			} else{							
				chosenList.add(movieInCart);//没有的话  将该电影加入购物车列表
				
			}         
		}
	}

解决

解决方法,直接在chosenList.add(movieInCart);这一语句后加一个break跳出while循环即可。
		while(it.hasNext()){			     //判断购物车列表里是否已经有该电影
			MovieInCart movieExisted = it.next();
			if(movieInCart.equals(movieExisted)){//有的话 仅更改已存在的该电影的购买数量
				movieExisted.setQuantity(movieExisted.getQuantity()+quantity);
			} else{							
				chosenList.add(movieInCart);//没有的话  将该电影加入购物车列表
				<strong>break;//程序若能执行到此,已将选择的电影添加,无需在进行下一次循环</strong>
			}         
		}
另外用增强的for循环的话,也一样加break;即解决问题,只是代码与用迭代器稍有不同,本文在后面也会讲到。
	for(MovieInCart movieExisted:chosenList){
		if(movieInCart.equals(movieExisted)){//有的话 仅更改已存在的该电影的购买数量
			movieExisted.setQuantity(movieExisted.getQuantity()+quantity);
			break; //有一个满足条件修改后的即退出循环</strong>
		} else{							
			chosenList.add(movieInCart);//没有的话  将该电影加入购物车列表	
			break; //添加后即退出循环	
		}
	}

进一步

ConcurrentModificationException意为并发的修改异常,也就是说ConcurrentModificationException容易在我们对ArrayList对象进行多种操作时出现,尤其是循环中。至此,我们所遇到的问题算是已经解决,读者如果只是想要不再看到这恼人的异常,可以就此结束本文的阅读,仔细看看自己的代码有没有用多次的不同的操作修改AraayList对象。
接下来,让我们深入源代码腹地,看看到底是发生了什么。
报错内容很明确地提供了信息:

从上面的报错信息我们可以看出,异常的始发地点是checkForComodification()方法,我们点击后面的提示找到该方法:
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
该方法在ArrayList类下的Itr类中,而Itr类是Iterator接口的实现类,也就是说,当我们对ArrayList用到迭代器时,调用迭代器的方法实际上是调用了Itr类的方法。从checkForComodification()方法中,我们看出异常出现的原因是 modCount != expectedModCount 。这俩又是什么呢?
讲到这,我们应该着手了解一下ArrayList类下的Itr类的一些属性了。
        int cursor;    // index of next element to return  下一元素的下标
        int lastRet = -1; // index of last element returned; -1 if no such   最后一个元素的下标,没有元素则为-1

        int expectedModCount; //初始为modCount,作用是使迭代器的修改次数与ArrayList的相同。
int modCount; //实际上这是AbstractList中的属性。The number of times this list has been structurally modified
//该列表被结构性的修改过的次数。structurally ,我们可以理解为任何改变其属性的操作都是structurally的,如next(), remove(), add(E e)等。
乍看上去,似乎记录修改次数并没有什么作用。实际上,modCount和expectedModCount的作用,就是用来判断是否出现了并发修改异常。因为用迭代器遍历的时候,在单线程中,代码开发者是不允许同时对数组列表进行增删等操作的。
如果没有modCount和expectedModCount,而我们又像本文最开始的程序那样调用了ArrayList的add()方法会怎样呢?
	public void add(E e) {
            checkForComodification();

            try {
                int i = cursor;
                ArrayList.this.add(i, e);
                <strong>cursor = i + 1;</strong>
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
查看add(E e)方法的代码我们可以知道,那会对迭代器的游标cusor进行+1操作,而同时add(E e)方法本质还是调用了add(int index, E e)两个参数的方法:
	public void add(int index, E e) {
            rangeCheckForAdd(index);
            checkForComodification();
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }

该方法又对ArrayList的size进行了+1操作。这样一来,迭代器本来可能在本次循环就遍历完了,但由于size又加了1,it.hasNext返回又是true,导致进入下一次循环。这样如果我们假设没有checkForComodification();来检查,那么就将刚刚添加上的电影又当作了已存在的电影,因此进入if语句,将电影数量加一倍,这个过程调用了一次next(),因此游标cursor+1,到下一次while(it.hasNext())判断cursor = size,循环方才停止。这显然不是我们想要的。
为避免上述这样的操作混乱,所以add()、remove()、next()等众多方法中才有了checkForComodification(),因此,有了ConcurrentModificationException。

至此,犹抱琵琶欲露还羞的ConcurrentModificationException终于完全现出芳容。只要我们注意不要在用迭代器的同时三番两次地“得罪”ArrayList对象,她就不会随便发脾气。
不过即使这样,我们还是可以在修改ArrayList上有较大的自由度,只要我们想,并且,只要我们够巧妙。

还有什么

笔者想,既然我们用迭代器会出现这种情况,那么改用增强的for循环呢?
将while循环修改,我们得到如下的代码:
	for(MovieInCart movieExisted:chosenList){
	<span style="white-space:pre">	</span>if(movieInCart.equals(movieExisted)){//有的话 仅更改已存在的该电影的购买数量
			movieExisted.setQuantity(movieExisted.getQuantity()+quantity);
		} else{							
			chosenList.add(movieInCart);//没有的话  将该电影加入购物车列表			
		}
	}
然而,运行程序再次调试,却出现了和用迭代器一模一样的出此同一位置的异常:

说明,增强的for循环的本质,还是迭代器,只不过是隐含的。所以我们还是要在add()方法后加上break; 。
另外,在再次调试中,笔者还发现,添加重复的电影时出现了问题,蝙蝠侠大战超人的数量,笔者先是输入了12,后输入了1,却出现了下图的情况。

  排查半天,笔者发现,是因为if语句中没有break;,所以程序又进入了下一次循环又执行了一次else语句。所以最终正确的代码应该是:
	for(MovieInCart movieExisted:chosenList){
		if(movieInCart.equals(movieExisted)){//有的话 仅更改已存在的该电影的购买数量
			movieExisted.setQuantity(movieExisted.getQuantity()+quantity);
			break; //有一个满足条件修改后的即退出循环</strong>
		} else{							
			chosenList.add(movieInCart);//没有的话  将该电影加入购物车列表
			break; //添加后即退出循环		
		}
	}

最后,不错,我们与 ConcurrentModificationException的会面还算愉快。
不过,尽管我们最终得到了满意的代码,但是在写程序时,我们还是要尽量避免在遍历动态数组的同时对其进行过多的修改操作。


2025-10-13 14:44:07.862 INFO 22784 --- [c_level_2_delay] o.a.k.c.c.internals.ConsumerCoordinator : [Consumer clientId=consumer_10.13.35.30_db84cdce-3855-4886-86df-60cfd8e5ec0a, groupId=delay] Notifying assignor about the new Assignment(partitions=[delay_topic_level_2-0]) 2025-10-13 14:44:07.862 INFO 22784 --- [c_level_2_delay] o.a.k.c.c.internals.ConsumerCoordinator : [Consumer clientId=consumer_10.13.35.30_db84cdce-3855-4886-86df-60cfd8e5ec0a, groupId=delay] Adding newly assigned partitions: delay_topic_level_2-0 2025-10-13 14:44:07.863 INFO 22784 --- [c_level_2_delay] com.tplink.smb.eventcenter.api.Handler : ending rebalance! 2025-10-13 14:44:07.867 INFO 22784 --- [c_level_2_delay] o.a.k.c.c.internals.ConsumerCoordinator : [Consumer clientId=consumer_10.13.35.30_db84cdce-3855-4886-86df-60cfd8e5ec0a, groupId=delay] Setting offset for partition delay_topic_level_2-0 to the committed offset FetchPosition{offset=17, offsetEpoch=Optional.empty, currentLeader=LeaderAndEpoch{leader=Optional[admin1-virtual-machine:9092 (id: 0 rack: null)], epoch=absent}} 2025-10-13 14:44:07.907 INFO 22784 --- [nPool-worker-15] c.t.nbu.demo.basicspringboot.DelayApp : 这是测试消息org.apache.kafka.clients.consumer.KafkaConsumer@7b0bfc10,1760337847906 2025-10-13 14:44:07.908 ERROR 22784 --- [nPool-worker-15] c.t.smb.eventcenter.core.DataProcessor : Fail to process event, handler:com.tplink.nbu.demo.basicspringboot.DelayApp$$Lambda$858/392795843 java.util.ConcurrentModificationException: KafkaConsumer is not safe for multi-threaded access at org.apache.kafka.clients.consumer.KafkaConsumer.acquire(KafkaConsumer.java:2445) ~[kafka-clients-2.8.0.jar:na] at org.apache.kafka.clients.consumer.KafkaConsumer.acquireAndEnsureOpen(KafkaConsumer.java:2429) ~[kafka-clients-2.8.0.jar:na] at org.apache.kafka.clients.consumer.KafkaConsumer.paused(KafkaConsumer.java:2048) ~[kafka-clients-2.8.0.jar:na] at com.tplink.nbu.demo.basicspringboot.DelayApp.lambda$registerEventConsumer$0(DelayApp.java:91) ~[classes/:na] at com.tplink.smb.eventcenter.core.DataProcessor.run(DataProcessor.java:31) ~[eventcenter.core-1.4.5002-test-SNAPSHOT.jar:1.4.5002-test-SNAPSHOT] at java.util.concurrent.ForkJoinTask$AdaptedRunnableAction.exec(ForkJoinTask.java:1386) [na:1.8.0_462-462] at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289) [na:1.8.0_462-462] at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056) [na:1.8.0_462-462] at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692) [na:1.8.0_462-462] at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:175) [na:1.8.0_462-462] private void registerEventConsumer() { EventHandler eventHandler = event -> { log.info("这是测试消息{},{}",eventCenter.getDelayConsumerByTopic(EVENT_TOPIC),System.currentTimeMillis()); eventCenter.getDelayConsumerByTopic(EVENT_TOPIC).paused(); try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } eventCenter.getDelayConsumerByTopic(EVENT_TOPIC).resume(new Collection<TopicPartition>() { @Override public int size() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean contains(Object o) { return false; } @Override public Iterator<TopicPartition> iterator() { return null; } @Override public Object[] toArray() { return new Object[0]; } @Override public <T> T[] toArray(T[] a) { return null; } @Override public boolean add(TopicPartition topicPartition) { return false; } @Override public boolean remove(Object o) { return false; } @Override public boolean containsAll(Collection<?> c) { return false; } @Override public boolean addAll(Collection<? extends TopicPartition> c) { return false; } @Override public boolean removeAll(Collection<?> c) { return false; } @Override public boolean retainAll(Collection<?> c) { return false; } @Override public void clear() { } }); log.info("这是测试消息{},{}",eventCenter.getDelayConsumerByTopic(EVENT_TOPIC),System.currentTimeMillis()); }; eventCenter.registerUnicast( EVENT_TOPIC, "delay", eventHandler, ForkJoinPool.commonPool(), PartitionAssignorMode.COOPERATIVE_STICKY ); } }请分析并解决
10-14
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值