记一次ConcurrentModificationExcepti
java相关的工程师,对ConcurrentModificationException
应该是很熟悉了。在并发中使用集合,经常一不小心就会碰到这个问题,大多数情况下,加上一些锁就能解决这个bug。而我今天碰到的,就是诡异的加锁之后发生的ConcurrentModificationException
。
示例
class Cache {
private final List<Person> persons = new ArrayList<>();
List<Person> getPersons() {
if (persons.isEmpty()) {
synchronized (this) {
if (persons.isEmpty()) {
for (int i = 0; i < 7; i++) {
// try {
// Thread.sleep(1);
//} catch (InterruptedException e) {
// e.printStackTrace();
//}
persons.add(new Person());
}
}
}
}
return Collections.unmodifiableList(persons);
}
}
getPersons
方法会在不同的线程调用。为了提高代码效率,特意模仿double-check的单例模式写了persons
集合初始化的过程。结果线上就发生了一例ConcurrentModificationException
异常。
百思不得其解。
原因
只能说这种写法初衷是好的,但东施效颦了。集合的初始化跟单例的初始化是有很大区别的。将注释的代码解注释,两个线程跑一下,很容易出错。
原因就在于在A线程执行for
循环体时,B线程执行外层persons.isEmpty
会返回false,导致B线程最终会拿到一个正在被A线程执行add
操作的集合(Collections.unmodifiableList
只是对传入的集合做了包装),这样如果B执行集合的遍历操作,就会发生ConcurrentModificationException
异常。
需要注意的是,以下代码也是不可以的:
static class Cache {
private List<Person> persons;
List<Person> getPersons() {
if (persons == null) {
synchronized (this) {
if (persons == null) {
persons = new ArrayList<>();
for (int i = 0; i < 7; i++) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
persons.add(new Person());
}
}
}
}
return Collections.unmodifiableList(persons);
}
}
依然有可能发生ConcurrentModificationException
异常。
解决方式也简单,去掉外层的if
逻辑就好了。
结语
可能是自己写的代码的缘故,找原因时一直没抓住关键点,最后都要直接try...catch...
一下了。还好求助了同事,总算是弄明白了。
对于Java的非受检异常,还是应该好好思考找到原因,不仅仅使代码更加健壮,也会学到很多东西。try...catch...
一时爽,也会断绝成长之路。有时候当局者迷,要学会放低姿态,向别人求助,三人行必有我师,古人诚不欺我也。