前言
本文是写在本人在网上找了很多资料也没有找到关于 “如何正确应对ConcurrentModificationException
这个异常的” 的前提, 本文均是作者自己的思考, 如有错误欢迎指出
开始
先决条件: 对java集合的fail-fast有一定了解
我们都知道, 在进行增强for循环迭代时, 不能直接用集合的remove方法, 这样会抛出java.util.ConcurrentModificationException
比如:
List l = new LinkedList<String>(){{
add("b");
add("b");
add("c");
}};
for (String s : l) {
if ("b".equals(s)) {
l.remove(s);
}
}
这样就会抛出异常, 正确的姿势是
Iterator<String> itr = l.iterator();
while (itr.hashNext() {
String s = itr.next();
if ("b".equals(s)) {
itr.remove(); // 利用迭代器移除元素
}
}
其他不在赘述, 具体源码分析推荐 这一篇博文
今天主要讨论在多线程环境下的快速失败机制及其作用
fail-fast定义
Definition of Failing Fast:
To fail fast means to have a process of starting work on a project, immediately gathering feedback, and then determining whether to continue working on that task or take a different approach—that is, adapt. If a project is not working, it is best to determine that early on in the process rather than waiting until too much money and time has been spent.
-- 机翻
快速失败意味着有一个开始项目工作的过程,立即收集反馈,然后决定是继续工作还是采取不同的方法ーー也就是适应。如果一个项目不起作用,最好在过程的早期就确定,而不要等到花费了太多的金钱和时间。
总的来说这是一个设计思想, 而非java中独有的东西, 目的是快速反馈, 然后再决策. 在java中, 这个反馈就是ConcurrentModificationException
这个异常
例子
LinkedList
并非一个线程安全的集合
假设有这样一个场景:
在线程一
中正在利用集合的迭代器, 线程二
正在修改这个集合
那么线程一
就有可能会发生意想不到的问题
下面的代码例子中list
初始化为["a", "b", "c"]
然后新建一个线程, 在此线程中利用list
的迭代器来进行迭代打印的操作, 并且迭代到"b"
这一项会导致此线程休眠一秒
主线程首先会休眠0.5秒, 然后修改集合, 添加一个元素"d"
, 然后打印集合
package test;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class TestThread {
public static void main(String[] args) throws InterruptedException {
LinkedList<String> list = new LinkedList<>(){{
add("a");
add("b");
add("c");
}};
new Thread(() -> {
Iterator<String> itr = list.iterator();
while (itr.hasNext()) {
String s = itr.next();
if ("b".equals(s)) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(s);
}
}).start();
Thread.sleep(500); // 由于迭代速度比添加元素快, 所以这里要让主线程休眠一下
list.add("d");
System.out.println(list);
}
}
嗯… 不出意外抛了ConcurrentModificationException
之前看到, 利用迭代器的remove()
方法可以对expectedModCount
重新赋值, 下面用一个不严谨不客观的测试看看这个能不能避免异常
if ("b".equals(s)) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
itr.remove(); // 改动在这里
}
是的, 不可以避免, 并且这个异常被提前抛出了, 即在itr.remove()
的时候就抛出了
但是为什么要抛出这样一个异常?
我想说下自己的想法:
首先我们想一下, 在对LinkedList集合进行增删之后继续用原来的iterator遍历是否合理?
私以为, 不合理, 他会产生逻辑上的错误, 比如有这样一个场景, LinkedList
的一个集合存放了账户交易账单(接下来简称list
), 接下来会对这些集合的所有元素进行迭代处理, 比如发短信 blabal…
然而迭代处理这个list
和添加这些list
的元素在操作两个不同的线程, 如果在迭代处理list
的过程中另一个线程又添加了元素, 那么不就会发生业务bug了吗? (比如有用户没收到短信)
ConcurrentModificationException
这个异常用来告知我们集合发生了变更
我们也可以处理这个异常, 比如可以捕获这个异常, 重新迭代
Iterator<String> itr = list.iterator();
while (itr.hasNext()) {
try {
String s = itr.next();
if ("b".equals(s)) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
itr.remove();
}
System.out.println(s);
} catch (ConcurrentModificationException e) { // 捕获这个异常, 重新迭代
itr = list.iterator();
}
}
当然这只是众多操作的一种
但是, 我认为我们不能依赖ConcurrentModificationException
, LinkedList
终究是线程不安全的集合, 多线程操作下我们还是需要选择线程安全集合, 如CopyOnWriteArrayList
和Vector
之类
假设我们把这里的LinkedList
换成CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>(){{
add("a");
add("b");
add("c");
}}; // 用这个集合后就不会抛出异常了!
最后
我认为ConcurrentModificationException
异常作用应该仅仅当作 “异常的操作”, 即告诉程序员们这样用是错误的, 会导致逻辑错误
当然决策权还是在程序员们, 到底怎么处理还应看实际情况
如有错误, 欢迎指正!