Java Vector集合ConcurrentModificationException()分析与解决办法

694 阅读3分钟

技术问题请发邮箱:justdi@foxmail.com

ConcurrentModificationException()异常分析

业务分析

前段时间项目业务中出现了一个问题,业务大致是在任务启动时会进行大量的查询以及修改,此处的查询不仅仅是get,而是通过stream流的方式进过一系列处理然后FindFirst(),为了加快效率就采用了内存的方式进行。大量的数据放入到了成员变量Vector中去,但是在某些时候任务启动时在大量查+FindFirst()时不仅会进行查询也会进行小部分的remove以及add,某些情况下就会导致ConcurrentModificationException异常。

深入源码

FindFirst()分析(大部分出错原因都类似,多线程导致期望值与修改值不同,解释向下看)

查看方法调用链会发现最终会进入到Vecrot的内部类中的tryAdvance(Consumer<? super E> action)方法中。

贴源码

  • xxx.stream()方法会初始化很多参数expectedModCount就是在此处初始化的,初始值为0.
default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
}
@Override
public Spliterator<E> spliterator() {
    return new VectorSpliterator<>(this, null, 0, -1, 0);
}
VectorSpliterator(Vector<E> list, Object[] array, int origin, int fence,
                          int expectedModCount) {
        this.list = list;
        this.array = array;
        this.index = origin;
        this.fence = fence;
        this.expectedModCount = expectedModCount;
}
@SuppressWarnings("unchecked")
        public boolean tryAdvance(Consumer<? super E> action) {
            int i;
            if (action == null)
                throw new NullPointerException();
            if (getFence() > (i = index)) {
                index = i + 1;
                action.accept((E)array[i]);
                if (list.modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                return true;
            }
            return false;
        }

此时会发现在执行tryAdvance(Consumer<? super E> action)方法时进行了一个判断if (list.modCount != expectedModCount)

  • list.modCount
  • expectedModCount表示对list的期望修改次数,详细请看上面的内容。

expectedModCount参数会在执行tryAdvance(Consumer<? super E> action)方法时执行getFence()方法进行修改->expectedModCount = list.modCount,list是每次初始化stream时通过new的方式将this进行传递,this就是当前Vector,说明了list.modCount每次都会初始化时都会发生改变。

解决办法

  • Vector更换为CopyOnWriteArrayList(手动滑稽),CopyOnWriteArrayList确实可以解决这个问题,但是它会copy一个副本,对副本进行处理,如果存在大量的副本对系统内存的消耗也是一个问题(留下了资本的眼泪)。

看一下CopyOnWriteArrayList官方怎么说的。

This is ordinarily too costly......the iterator is guaranteed not to throw {@code ConcurrentModificationException}

This is ordinarily too costly, but may be <em>more</em> efficient
 * than alternatives when traversal operations vastly outnumber
 * mutations, and is useful when you cannot or don't want to
 * synchronize traversals, yet need to preclude interference among
 * concurrent threads.  The "snapshot" style iterator method uses a
 * reference to the state of the array at the point that the iterator
 * was created. This array never changes during the lifetime of the
 * iterator, so interference is impossible and the iterator is
 * guaranteed not to throw {@code ConcurrentModificationException}.
 * The iterator will not reflect additions, removals, or changes to
 * the list since the iterator was created.  Element-changing
 * operations on iterators themselves ({@code remove}, {@code set}, and
 * {@code add}) are not supported. These methods throw
 * {@code UnsupportedOperationException}.
  • 多线程并发处理

1 stream流的处理

1.1 因为stream流的findFirst()涉及到了modCountexpectedModCount,所以针对“这个Vector”,不仅要对updateremove进行加锁防止多线程修改modCount的值,也要对查询进行加锁,加锁可以使用synchronized或者lock看个人选择。

2 普通处理

2.1 仅需对updateremove进行加锁,查询采用根据数组下标的方式get(int Index)

get()源码,不涉及modCount的修改

public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);

        return elementData(index);
}
@SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
}