下面通过一段简单的代码复现开发需求过程中遇到的问题:
public class Test {
private static Map<String, List<String>> LIST_MAP = new HashMap<>();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
List<String> list = fetchList();
Collections.sort(list);
for (String e : list) {
System.out.println(e);
}
});
thread.start();
}
}
private static List<String> fetchList() {
if (!CollectionUtils.isEmpty(LIST_MAP)) {
return LIST_MAP.get("key");
}
refreshListMap();
return LIST_MAP.get("key");
}
private static void refreshListMap(){
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
LIST_MAP.put("key", list);
}
}
代码运行时,第10行和第11行交替报出异常,具体异常为ConcurrentModificationException。看下代码逻辑,其实就是先获取到一个集合,对集合先进行一次排序,在遍历集合。可是为啥会报并发修改异常,有以下几个疑问点:
- Collections.sort()方法对集合进行排序,为啥会出现并发修改的异常?
- 通过增强for循环遍历集合,并没有去修改集合的内部结构,为啥会出现并发修改的异常?
通常对于这种异常,个人喜欢去看源码,结合源码分析效果会更高。依次看下【增强for循环】以及【Collections.sort()】各自的内部实现
增强for循环
- 增强for循环内部原理其实是一个 Iterator 迭代器
- 迭代器主要用于遍历Collection集合中的元素,即所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象
首先我们进入 AbstractList的iterator() 方法 , 这里iterator是返回了了一个新对象Itr
public Iterator<E> iterator() {
return new Itr();
}
Itr是AbstractList的一个内部类,来看一下Itr这个类的具体内容
private class Itr implements Iterator<E> {
/**
* Index of element to be returned by subsequent call to next.
*/
int cursor = 0;
/**
* Index of element returned by most recent call to next or
* previous. Reset to -1 if this element is deleted by a call
* to remove.
*/
int lastRet = -1;
/**
* The modCount value that the iterator believes that the backing
* List should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
关注下【int expectedModCount = modCount】,其中modCount代表对集合的实际操作次数 expectedModCount代表对集合的预期操作次数,初始时expectedModCount=modCount,后续的检查都是基于这两个数值。注意到迭代器中的next方法,在方法的一开始执行了checkForComodification方法,方法内容如下:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这个方法会抛出ConcurrentModificationException异常,前提是modCount != expectedModCount,那也就是在遍历集合的时候,线程中执行的其他方法修改了modCount或者expectedModCount的值
Collections.sort()
介绍完了【增强for循环】,下面再说下集合的sort方法,sort方法的实现如下:
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
同样的,在对集合排序时,也会校验modCount和expectedModCount,不相同的话也会抛出ConcurrentModificationException异常。但这个都不是重点,重点是sort方法的最后一步是modCount++,修改了modCount的值
原先以为只有对集合进行如add、remove等方法才会导致modCount增加,没想到对集合进行sort、clear也会导致modCount++,看来只要改变了集合中的元素个数、元素顺序都算对集合进行了结构性修改
看了【增强for循环】以及【Collections.sort()】两者的内部实现,问题就迎刃而解了。
- 原因一:这两步操作都会判断集合的修改次数
- 原因二:sort完集合后,集合的修改次数会自增1
- 原因三:fetchList方法返回的集合都是同一个对象
综合以上三个原因,多线程并发执行的情况下,就会交替抛出ConcurrentModificationException异常。具体的解决方案如下:
- 每个线程都new一个集合出来,保证多个线程不是操作同一个集合,同一个集合的话,维护的modCount和expectedModCount也都是同一份
- 不直接对集合本身进行排序,例如通过stream流对集合进行排序,返回一个排序好的新集合,避免对集合进行操作后导致modCount和expectedModCount被修改