技术问题请发邮箱:justdi@foxmail.com
ConcurrentModificationException()异常分析
业务分析
前段时间项目业务中出现了一个问题,业务大致是在任务启动时会进行大量的查询以及修改,此处的查询不仅仅是get,而是通过stream流的方式进过一系列处理然后FindFirst(),为了加快效率就采用了内存的方式进行。大量的数据放入到了成员变量Vector中去,但是在某些时候任务启动时在大量查+FindFirst()时不仅会进行查询也会进行小部分的remove以及add,某些情况下就会导致ConcurrentModificationException异常。
深入源码
FindFirst()分析(大部分出错原因都类似,多线程导致期望值与修改值不同,解释向下看)
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.modCountexpectedModCount表示对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()涉及到了modCount和expectedModCount,所以针对“这个Vector”,不仅要对update和remove进行加锁防止多线程修改modCount的值,也要对查询进行加锁,加锁可以使用synchronized或者lock看个人选择。
2 普通处理
2.1 仅需对update和remove进行加锁,查询采用根据数组下标的方式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];
}