文中ConcurrentModificationException统一称为并发修改异常
什么是并发修改异常
官方解释如下:
public class ConcurrentModificationException
extends RuntimeException
This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.
For example, it is not generally permissible for one thread to modify a Collection while another thread is iterating over it. In general, the results of the iteration are undefined under these circumstances. Some Iterator implementations (including those of all the general purpose collection implementations provided by the JRE) may choose to throw this exception if this behavior is detected. Iterators that do this are known as fail-fast iterators, as they fail quickly and cleanly, rather that risking arbitrary, non-deterministic behavior at an undetermined time in the future.
Note that this exception does not always indicate that an object has been concurrently modified by a different thread. If a single thread issues a sequence of method invocations that violates the contract of an object, the object may throw this exception. For example, if a thread modifies a collection directly while it is iterating over the collection with a fail-fast iterator, the iterator will throw this exception.
Note that fail-fast behavior cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast operations throw
ConcurrentModificationException
on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness:ConcurrentModificationException
should be used only to detect bugs.
大概意思是当一个方法监测到一个不允许被修改的对象正在被并发修改(修改集合结构)时,就会报这个异常。这个异常在单线程和多线程运行环境都有可能发生。某个线程在 Collection 上进行迭代时,通常不允许另一个线程修改该Collection。通常在这些情况下,迭代的结果是不确定的。如果检测到这种行为,一些迭代器实现(包括JRE提供的所有通用collection实现)可能选择抛出此异常。
执行该操作的迭代器称为快速失败迭代器,因为迭代器很快就完全失败,而不会冒着在将来某个时间任意发生不确定行为的风险。迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败操作会尽最大努力抛出ConcurrentModificationException。
fail-fast 和 fail-safe
这里有必要解释下快速失败(fail-fast)和 快速成功(fail-safe)
fail-fast
官方解释是:
当Iterator这个迭代器被创建后,除了迭代器本身的方法(remove)可以改变集合的结构外,其他的因素如若改变了集合的结构,都被抛出ConcurrentModificationException异常。
所以只要改变了集合结构就会抛出并发修改异常,所谓修改集合结构,比如集合上的插入和删除就是结构上的改变,但是,如果是对集合中某个元素进行修改的话,并不是结构上的改变。
fail-safe
跟fail-fast相比,当集合的结构被改变的时候,fail-safe机制会在复制原集合的一份数据出来,然后在复制的那份数据遍历, 比如CopyOnWriteArrayList。但是相应的就存在两个缺点:
-
复制时需要消耗额外的时间和空间
-
不能保证遍历的最新的内容
迭代器和for循环的区别
先说for循环,for循环是依据每个集合提供的对集合类的操作方法对集合内对象的增删改查。
迭代器是Java提供的一种对序列类型数据结构的统一模式,不用关心底层结构是什么样子。但是迭代器遍历元素时,除了查看之外,只能做remove操作。但是删除元素这里也要注意,用集合的remove方法删除元素会触发并发修改异常,但是如果用迭代器的remove就不会;
为什么会发生并发修改异常&如何解决
其实前面大概也说了几种,比如触发并发修改异常分为单线程和多线程情况
单线程运行环境
public class Demo1 {
public static void main(String[] args) {
List<Obj> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new Obj(i));
}
// 遍历时删除元素
for (Obj obj : list) {
if (obj.getValue() < 10) {
// 这里会抛出ConcurrentModificationException
list.remove(obj);
}
}
}
@Getter
@Setter
@AllArgsConstructor
static
class Obj {
int value;
}
}
解决方案也很简单:单线程环境中可以通过将ArrayList集合改为CopyOnWriteArrayList,或者可以通过迭代器遍历删除,可以避免出现ConcurrentModificationException异常.
CopyOnWriteArrayList:也就是前面说的fail-safe
public class Demo1 {
public static void main(String[] args) {
CopyOnWriteArrayList<Obj> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(new Obj(i));
}
// 遍历时删除元素
for (Obj obj : list) {
if (obj.getValue() < 10) {
// 这里会抛出ConcurrentModificationException
list.remove(obj);
}
}
}
@Getter
@Setter
@AllArgsConstructor
static
class Obj {
int value;
}
}
迭代器遍历
public class Demo1 {
public static void main(String[] args) {
CopyOnWriteArrayList<Obj> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(new Obj(i));
}
/** 遍历时删除元素*/
Iterator<Obj> iterator = list.iterator();
while (iterator.hasNext()) {
Obj testObj = iterator.next();
if (testObj.getValue() < 10) {
iterator.remove();
}
}
}
@Getter
@Setter
@AllArgsConstructor
static
class Obj {
int value;
}
}
但是迭代器遍历删除的效率会高很多,所以一般也不推荐第一种方法
多线程运行环境
public class Demo4 {
public static void main(String[] args) {
/** 初始化集合类*/
ArrayList<Obj> list = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
list.add(new Obj(i));
}
/**启动一个线程随机删除数据*/
new Thread(new ThreadClass(list)).start();
/** 遍历元素*/
Iterator<Obj> iterator = list.iterator();
while (iterator.hasNext()) {
iterator.next();
}
}
}
class ThreadClass implements Runnable {
List<Obj> list;
public ThreadClass(List<Obj> list) {
this.list = list;
}
@Override
public void run() {
while (true) {
int index = new Random().nextInt(list.size());
list.remove(index);
}
}
}
@Getter
@Setter
@AllArgsConstructor
class Obj {
int value;
}
多线程环境下将将ArrayList改为CopyOnWriteArrayList在多线程环境中同样可以避免出现这个异常,也就是说fail-safe同样有用,但是迭代器遍历失去了作用。
线上问题出现并发修改异常有很多是HashMap及其扩展类触发的,这种时候可以换成线程安全的HashMap类,如ConcurrentHashMap,这个时候就可以避免出现ConcurrentModificationException异常。
参考:
https://zhuanlan.zhihu.com/p/37345813
原文始发于微信公众号(子枫进阶之路):常见线上问题之ConcurrentModificationException
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/24473.html