Exception in thread "main" java.util.ConcurrentModificationException

本贴最后更新于 1893 天前,其中的信息可能已经时移世改

遍历 List 是我们在写代码中经常碰到的,有时候我们会碰到想在遍历的途中删掉某些元素的需求,但一不小心可能就会报快速失败错误(FastFail),这其实跟 List 当中的一些实现有关,下面我选取了两种情况,来自知乎大神的回答,加上自己的疑惑和总结。

作者:RednaxelaFX

链接:https://www.zhihu.com/question/56586732/answer/149650876

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结下就是两种情况:一种是可以删除末尾的元素,另一种是在倒数第二时可以删除任何一个元素。

我觉得题主的首要问题是被自己写的代码(以及 JDK 的 ArrayList 那可恶的 API)给坑了。

ArrayList 上有两个版本的 remove 方法:

public E remove(int index)

public boolean remove(Object o)

题主很可能以为自己调用的是第二个版本,但实际上调用的是第一个版本——remove(3) 删除了位于末尾的元素,而不是位于倒数第二的元素。

ArrayList.iterator() 返回出来的 Iterator,里面的 hasNext()是不关心 modification count 的,而 next()会去检查 modification count:

List 中的内部类

/**

* An optimized version of AbstractList.Itr

*/

private class Itr implements Iterator {

	int cursor;       // index of next element to return

	int lastRet = -1; // index of last element returned; -1 if no such

	int expectedModCount = modCount;

	public boolean hasNext() {

		return cursor != size;

	}

	@SuppressWarnings("unchecked")

	public E next() {

		checkForComodification();

		int i = cursor;

		if (i >= size)

			throw new NoSuchElementException();

		Object[] elementData = ArrayList.this.elementData;

		if (i >= elementData.length)

			throw new ConcurrentModificationException();

		cursor = i + 1;

		return (E) elementData[lastRet = i];

	}

	public void remove() {

		if (lastRet < 0)

			throw new IllegalStateException();

		checkForComodification();

		try {

			ArrayList.this.remove(lastRet);

			cursor = lastRet;

			lastRet = -1;

			expectedModCount = modCount;

		} catch (IndexOutOfBoundsException ex) {

			throw new ConcurrentModificationException();

		}

	}

	final void checkForComodification() {

		if (modCount != expectedModCount)

			throw new ConcurrentModificationException();

	}

}

所以题主的那个程序实际做的事情就是:

  • 通过 ArrayList.iterator() 得到了一个新的 iterator,开始遍历

  • list.remove(3):删除了位于 index 3 的元素(Integer.valueOf(4) 得到的对象)

  • 然后调用 iterator.hasNext(),得到 false,于是就退出了循环而没有去执行那个会检查 modification count 的 next()方法。

ArrayList 的 JavaDoc 说:

The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

没有特别指定 Iterator 里的哪些方法一定会根据 fail-fast 原则而抛异常。但 Iterator.hasNext()的 JavaDoc 说:

hasNext

boolean hasNext()

Returns true if the iteration has more elements. (In other words, returns true if next() would return an element rather than throwing an exception.)

Returns:true if the iteration has more elements

就这个规定来说,我觉得题主观察到的现象应该算是 JDK 实现的上的巧合:因为如果在这个位置调用 next()的话会抛 ConcurrentModificationException 异常,所以 hasNext()也要返回 false,于是就好啦。

但 JDK 这个具体实现看起来还是有 bug,应该让 hasNext()也做 checkForComodification()的不抛异常对应动作才对。

就这样。

还是未能解决下面的疑问:

在判断的是倒数第二个的时候,所有的元素都能删。

List list = new ArrayList();

list.add("a");

list.add("b");

list.add("c");

list.add("d");

list.add("e");

list.add("f");



Iterator it = list.iterator();

while (it.hasNext()) {

	String itt = (String) it.next();

	if (itt.equals("e")) {

		list.remove("a");

	}

}

for (int i = 0; i < list.size(); i++) {

	System.out.println(list.get(i));

}

下面是解释:

作者:xRay

链接:https://www.zhihu.com/question/56916067/answer/151995061

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

为什么没抛异常,跟下代码其实就一清二楚了。

ArralyList 有个版本号 modCount,每次修改 ArrayList 时版本号就会往上加。Iterator 里面也有个版本号 expectedModCount 它的初始值就是 modCount。只有 expectedModCount != modCount 才会抛异常。所以 printList 里面的绝对不会抛异常。那问题就出在进行 remove 的那个迭代器上,首先迭代器是在什么时候比较这两个值呢? 答案是在 remove 跟 next 的时候,也就是说 hasNext 它不会抛出这个异常。

List 中的删除方法:

/**

 * Removes the element at the specified position in this list.

 * Shifts any subsequent elements to the left (subtracts one from their

 * indices).

 *

 * @param index the index of the element to be removed

 * @return the element that was removed from the list

 * @throws IndexOutOfBoundsException {@inheritDoc}

 */

public E remove(int index) {

	rangeCheck(index);

	modCount++;

	E oldValue = elementData(index);

	int numMoved = size - index - 1;

	if (numMoved > 0)

		System.arraycopy(elementData, index+1, elementData, index,

						 numMoved);

	elementData[--size] = null; // clear to let GC do its work

	return oldValue;

}

然后,看下题主的这段代码

Iterator iterator = list.iterator();

while(iterator.hasNext()) {

	Integer integer = iterator.next();

	if(integer == 2)

		list.remove(integer);

}

这里面调用了 List 的 remove 方法,所以它不会抛出 ConcurrentModificationException。问题是 remove 之后为什么 hasNext 会返回 false。我们看下 hasNext 方法

int cursor; // index of next element to return

public boolean hasNext() { return cursor != size; }

看注释,cursor 是指向一个元素的,也就是当 list.remove(integer=2)的时候,cursor 实际等于 2。而 list.remove(integer)后 size=3-1=2,所以 hasNext 返回 false,也就不会执行 next 方法,所以也就不会抛出 ConcurrentModificationException。

不过,这个问题,更有意思的 ArrayList 有两个 remove 方法

public E remove(int index) boolean remove(Object o)

当参数是 Integer 时会调用哪个? 我在 JLS 中找到这么一段,意思是选择重载函数时不会优先考虑装箱跟拆箱

The first phase (§15.12.2.2) performs overload resolution without permitting boxing or unboxing conversion, or the use of variable arity method invocation. If no applicable method is found during this phase then processing continues to the second phase.

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3165 引用 • 8206 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • visus

    同步写同步读,试一试用 COW 模式

推荐标签 标签

  • SOHO

    为成为自由职业者在家办公而努力吧!

    7 引用 • 55 回帖 • 99 关注
  • 单点登录

    单点登录(Single Sign On)是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    9 引用 • 25 回帖 • 9 关注
  • Sillot

    Sillot (汐洛)孵化自思源笔记,致力于服务智慧新彖乄,具有彖乄驱动、极致优雅、开发者友好的特点
    Github 地址:https://github.com/Hi-Windom/Sillot

    12 引用 • 26 关注
  • 微信

    腾讯公司 2011 年 1 月 21 日推出的一款手机通讯软件。用户可以通过摇一摇、搜索号码、扫描二维码等添加好友和关注公众平台,同时可以将自己看到的精彩内容分享到微信朋友圈。

    129 引用 • 791 回帖 • 1 关注
  • 面试

    面试造航母,上班拧螺丝。多面试,少加班。

    324 引用 • 1395 回帖
  • Rust

    Rust 是一门赋予每个人构建可靠且高效软件能力的语言。Rust 由 Mozilla 开发,最早发布于 2014 年 9 月。

    57 引用 • 22 回帖
  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1090 引用 • 3467 回帖 • 297 关注
  • NetBeans

    NetBeans 是一个始于 1997 年的 Xelfi 计划,本身是捷克布拉格查理大学的数学及物理学院的学生计划。此计划延伸而成立了一家公司进而发展这个商用版本的 NetBeans IDE,直到 1999 年 Sun 买下此公司。Sun 于次年(2000 年)六月将 NetBeans IDE 开源,直到现在 NetBeans 的社群依然持续增长。

    78 引用 • 102 回帖 • 636 关注
  • 周末

    星期六到星期天晚,实行五天工作制后,指每周的最后两天。再过几年可能就是三天了。

    14 引用 • 297 回帖
  • NGINX

    NGINX 是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。 NGINX 是由 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的,第一个公开版本 0.1.0 发布于 2004 年 10 月 4 日。

    311 引用 • 546 回帖 • 57 关注
  • Kafka

    Kafka 是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是现代系统中许多功能的基础。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。

    35 引用 • 35 回帖
  • 黑曜石

    黑曜石是一款强大的知识库工具,支持本地 Markdown 文件编辑,支持双向链接和关系图。

    A second brain, for you, forever.

    9 引用 • 83 回帖
  • 倾城之链
    23 引用 • 66 回帖 • 93 关注
  • 反馈

    Communication channel for makers and users.

    123 引用 • 906 回帖 • 177 关注
  • Webswing

    Webswing 是一个能将任何 Swing 应用通过纯 HTML5 运行在浏览器中的 Web 服务器,详细介绍请看 将 Java Swing 应用变成 Web 应用

    1 引用 • 15 回帖 • 633 关注
  • SMTP

    SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。

    4 引用 • 18 回帖 • 581 关注
  • MongoDB

    MongoDB(来自于英文单词“Humongous”,中文含义为“庞大”)是一个基于分布式文件存储的数据库,由 C++ 语言编写。旨在为应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。

    90 引用 • 59 回帖 • 4 关注
  • OpenShift

    红帽提供的 PaaS 云,支持多种编程语言,为开发人员提供了更为灵活的框架、存储选择。

    14 引用 • 20 回帖 • 596 关注
  • Electron

    Electron 基于 Chromium 和 Node.js,让你可以使用 HTML、CSS 和 JavaScript 构建应用。它是一个由 GitHub 及众多贡献者组成的活跃社区共同维护的开源项目,兼容 Mac、Windows 和 Linux,它构建的应用可在这三个操作系统上面运行。

    15 引用 • 136 回帖 • 1 关注
  • LaTeX

    LaTeX(音译“拉泰赫”)是一种基于 ΤΕΧ 的排版系统,由美国计算机学家莱斯利·兰伯特(Leslie Lamport)在 20 世纪 80 年代初期开发,利用这种格式,即使使用者没有排版和程序设计的知识也可以充分发挥由 TeX 所提供的强大功能,能在几天,甚至几小时内生成很多具有书籍质量的印刷品。对于生成复杂表格和数学公式,这一点表现得尤为突出。因此它非常适用于生成高印刷质量的科技和数学类文档。

    9 引用 • 32 回帖 • 179 关注
  • 旅游

    希望你我能在旅途中找到人生的下一站。

    83 引用 • 894 回帖
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    284 引用 • 4481 回帖 • 652 关注
  • 机器学习

    机器学习(Machine Learning)是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、算法复杂度理论等多门学科。专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识或技能,重新组织已有的知识结构使之不断改善自身的性能。

    76 引用 • 37 回帖
  • OnlyOffice
    4 引用 • 19 关注
  • Hibernate

    Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。

    39 引用 • 103 回帖 • 676 关注
  • SendCloud

    SendCloud 由搜狐武汉研发中心孵化的项目,是致力于为开发者提供高质量的触发邮件服务的云端邮件发送平台,为开发者提供便利的 API 接口来调用服务,让邮件准确迅速到达用户收件箱并获得强大的追踪数据。

    2 引用 • 8 回帖 • 429 关注
  • 安装

    你若安好,便是晴天。

    128 引用 • 1183 回帖