记一次ConcurrentModificationException的遭遇

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...一时爽,也会断绝成长之路。有时候当局者迷,要学会放低姿态,向别人求助,三人行必有我师,古人诚不欺我也。

猜你喜欢

转载自blog.csdn.net/weixin_34025051/article/details/87346802