当前位置:

记一次多线程遍历ArrayList,报java.util.ConcurrentModificationException异常

本次仅是多线程遍历,不涉及删改。摘记:
1、多线程环境选择CopyOnWriteArrayList、ConcurrentHashMap
2、CopyOnWriteArrayList只能用list.remove(), 不能使用Iterator.remove()进行删除。
解决方案出自CSDN文章。原文如下:
Java遍历 List 和 Map 出现 ConcurrentModificationException 异常原因分析及解决方法
一、单线程

异常情况举例
只要抛出出现异常,可以肯定的是代码一定有错误的地方。先来看看都有哪些情况会出现ConcurrentModificationException异常,下面以ArrayList的remove操作进行举例:
使用的数据集合:
List myList = new ArrayList();
myList.add( “1”); myList.add( “2”);
myList.add( “3”); myList.add( “4”);
以下三种情况都会出现异常:

 Iterator it = myList.iterator();
  while (it.hasNext()) {
      String value = it.next();
       if (value.equals( "3")) {
           myList.remove(value);  // ConcurrentModificationException
      }
 }

 for (Iterator it = myList.iterator(); it.hasNext();) {
     String value = it.next();
      if (value.equals( "3")) {
          myList.remove(value);  // ConcurrentModificationException
     }
 }

 for (String value : myList) {
     System. out.println( "List Value:" + value);
      if (value.equals( "3")) {
          myList.remove(value);  // ConcurrentModificationException
     }
 }   


异常信息如下:

Exception in thread “main” java.util.ConcurrentModificationException at java.util.AbstractList…
根本原因
以上都有3种出现异常的情况有一个共同的特点,都是使用Iterator进行遍历,且都是通过ArrayList.remove(Object) 进行删除操作。
想要找出根本原因,直接查看ArrayList源码,原因是modCount不等于expectedModCount,则抛出ConcurrentModificationException异常。

List、Set、Map 都可以通过Iterator进行遍历,这里仅仅是通过List举例,在使用其他集合遍历时进行增删操作都需要留意是否会触发ConcurrentModificationException异常。

解决方案
上面列举了会出现问题的几种情况,也分析了问题出现的根本原因,现在来总结一下怎样才是正确的,如果避免遍历时进行增删操作不会出现ConcurrentModificationException异常。

// 1 使用Iterator提供的remove方法,用于删除当前元素

 Iterator it = myList.iterator();
 while (it.hasNext()) {
     String value = it.next();
        if (value.equals( "3")) {
              it.remove();  // ok
       }
 }
 System. out.println( "List Value:" + myList.toString());

// 2 建一个集合,记录需要删除的元素,之后统一删除

List templist = new ArrayList();
for (String value : myList) {
     if (value.equals( "3")) {
         templist.add(value);
    }
}
myList.removeAll(templist);
System.out.println( "List Value:" + myList.toString());  

// 3. 使用线程安全CopyOnWriteArrayList进行删除操作

  List myList = new CopyOnWriteArrayList();
  Iterator it = myList.iterator();
  while (it.hasNext()) {
      String value = it.next();
      if (value.equals( "3")) {
          myList.remove(value);
          //it.remove();  
          // 注意CopyOnWriteArrayList不能用Iterator的remove方法,
          否则UnsupportedOperationException异常;
          //ArrayList不能直接用remove,只能用用Iterator的remove方法,
          否则ConcurrentModificationException异常;
     }
  }

输出结果都是:List Value:[1, 2, 4] , 不会出现异常。
以上3种解决办法在单线程中测试完全没有问题,但是如果在多线程中呢?

二、多线程

同步异常情况举例
上面针对ConcurrentModificationException异常在单线程情况下提出了3种解决方案,但是如果涉及到多线程环境可能就不那么乐观了。
下面的例子中开启两个子线程,一个进行遍历,另外一个有条件删除元素:
  List<String> list = new ArrayList<>();
  list.add("1"); list.add("2"); list.add("3"); list.add("4");
  Thread thread1 = new Thread() {
      public void run() {
          Iterator<String> iterator = list.iterator();
          while (iterator.hasNext()) {
              String value = iterator.next();
              try {
                  Thread.sleep(100);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  };
  Thread thread2 = new Thread() {
      public void run() {
          Iterator<String> iterator = list.iterator();
          while (iterator.hasNext()) {
              String value = iterator.next();
              if (value.equals("3")) {
                  iterator.remove();
              }
              try {
                  Thread.sleep(100);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  };
  thread1.start();
  thread2.start();
  try {
      Thread.sleep(1000);
  } catch (InterruptedException e) {
      e.printStackTrace();
  }
  System.out.println("size is " + list.size());

Exception in thread “Thread-0” java.util.ConcurrentModificationException at java.util.AbstractList…

结论:
上面的例子在多线程情况下,使用单线程的第1种解决方案iterator.remove()进行删除依然出现异常ConcurrentModificationException,测试得知使用单线程的第2种解决方案也会出现同样的问题。
接着来再看一下JavaDoc对java.util.ConcurrentModificationException异常的描述:
当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。

尝试方案
(1) 在所有遍历增删地方都加上synchronized或者使用Collections.synchronizedList,虽然能解决问题但是并不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
(2) 推荐使用ConcurrentHashMap(对应Map)或者CopyOnWriteArrayList(对应List)。

注意事项 :
CopyOnWriteArrayList只能用list.remove(), 不能使用Iterator.remove()进行删除。会出现如下异常:
java.lang.UnsupportedOperationException: Unsupported operation remove
at java.util.concurrent.CopyOnWriteArrayList

多线程解决方案如下

List<String> myList = new CopyOnWriteArrayList<>();
myList.add("1"); myList.add("2");
myList.add("3"); myList.add("4");
Thread thread1 = new Thread() {
    public void run() {
        Iterator<String> iterator = myList.iterator();
        while (iterator.hasNext()) {
            String value = iterator.next();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
};
Thread thread2 = new Thread() {
    public void run() {
        Iterator<String> iterator = myList.iterator();
        while (iterator.hasNext()) {
            String value = iterator.next();
            if (value.equals("3")) {
                myList.remove(value);
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
};
thread1.start();
thread2.start();
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("size is " + myList.size());

#最后HashMap在多线程情况下:开启两个子线程,一个进行遍历,另外一个有条件删除元素报错代码,解决办法是使用ConcurrentHashMap,它是线程安全的,同一时刻只能有一个线程持有锁。

public static Map<String, String> maps = new HashMap<>();
public static void main(String[] args) {
    Thread1 a1 = new Thread1();
    new Thread(a1, "xc1").start();
    Thread2 a2 = new Thread2();
    new Thread(a2, "xc2").start();
}

static class Thread1 implements Runnable {
    @Override
    public void run() {
        System.out.println("thread1 start");
        for (int i = 0; i < 1000; i++) {
            maps.put(String.valueOf(i), String.valueOf(i));
            if (i == 99) {
                System.out.println("thread1 start" + maps.size());
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                }
            }
        }
        System.out.println("thread1 end");
    }
}

static class Thread2 implements Runnable {
    @Override
    public void run() {
        System.out.println("thread2 start");
        Set<String> keys = maps.keySet();
        Iterator<String> iter = maps.keySet().iterator();
        while (iter.hasNext()) {
            String key = iter.next();
            String value = maps.get(key);
            if (value.equals("99")) {
                System.out.println("thread2 start key" + keys.size());
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                }
            }
        }
        System.out.println("thread2 end");
    }
}

————————————————
版权声明:本文为CSDN博主「鸿蒙开发者」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_33391863/article/details/107601831

光锥极客  2023-07-18  阅读量:1074