【问题标题】:Why an exception is thrown when removing an item via iterator while running over the list?为什么在遍历列表时通过迭代器删除项目时会引发异常?
【发布时间】:2021-03-08 13:36:44
【问题描述】:

我调查了 ListIterator 类的 remove 方法的代码,但我不明白为什么在运行列表时删除项目后,尝试时会抛出异常获取删除后的下一个元素。 这是我读过的来源:

   public void remove() {

                if (lastRet < 0)


                    throw new IllegalStateException();


                checkForComodification();

                try {
                    SubList.this.remove(lastRet);

                    cursor = lastRet;
                    lastRet = -1;
                    expectedModCount = ArrayList.this.modCount;


                } catch (IndexOutOfBoundsException ex) {


                    throw new ConcurrentModificationException();
                }
            }

我无法理解的事情:

  1. 为什么 remove 方法得到 lastRet 而不是 cursor。我们要删除当前项目,而不是我们已经传递的项目?

2.为什么lastRet设置为-1?

3.以下行:expectedModCount = ArrayList.this.modCount; 将 *expectedModCount * 设置为等于 modCount,因此在下一次迭代中,当两个变量都将被检查是否相等时,if 将“说”true 并且一切正常。 我在网上看了很多文章,在SO中也有一些答案,但还是看不懂

因为我得到的响应说代码不会导致运行时异常,所以这里是导致它的代码:

    public class Testing
{
    public static void main(String[] args)
    {
        List <String> list = new ArrayList<String>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");

        Iterator<String> iterator = list.listIterator();
        while (iterator.hasNext())
        {
            String st = iterator.next();
            if (st.equals("c"))
            {
                list.remove(index);
            }

            else
            {
                System.out.println(st);
            }
        }
}

【问题讨论】:

  • 在调用 Iterator 的 remove 方法后,我从未从 Iterator 收到异常。考虑编辑您的问题并包含一个minimal reproducible example,该minimal reproducible example 演示了在调用 Iterator.remove 后 Iterator.next 如何引发异常。
  • @VGR,我添加了导致运行时异常的代码
  • 你没有调用Iterator的remove方法。您正在调用 List 的 remove 方法。在迭代集合时更改集合被记录为可能导致 ConcurrentModificationException。
  • @VGR,我明白了!谢谢!

标签: java generics arraylist iterator


【解决方案1】:
  1. Iterator.remove() 的 javadoc 说:
Removes from the underlying collection the last element returned by this iterator

所以当这个方法被调用时,光标指向下一个元素,因此使用了 lastRet。

  1. -1 表示未找到。它设置为 -1 因为您刚刚删除了 lastRet 因此它不再存在。这意味着你不能在不先调用 next() 的情况下再次调用 remove():如果你查看 next(),它会更新 lastRet

  2. 调用 ArrayList.this.remove() 会更改 modCount(modCount 由 AbstractList 管理,它是您正在查看的 ArrayList 迭代器的超类)。由于此更改是由方法本身引起的,因此我们知道这是一个有效的更改(换句话说,我们只是请求更改列表)。所以局部变量(expectedModCount)被更新了。 如果这个列表被迭代器的另一个实例修改,那么 AbstractList 的 modCount 会改变,但属于当前实例的 expectedModCount 不会改变。这样当前实例就会知道有并发修改。


补充问题:

我明白了。请只是一个小问题.. 如果我有两个不同的线程在同一个列表上运行 for/while 循环(而不是使用迭代器)。其中一个人删除了一个项目,在这种情况下不会发生异常?只有在使用 Iterators 系统时才“安全”?

多线程不是一个“小”问题:)

简短的回答是,不,这两种情况都不安全。无论您使用什么循环,因为 1 个线程正在检查执行另一个循环是否安全(无论是 .hasNext() 还是 i

你应该自己试试。以下是一些 hacky 示例(我使用 LinkedList 进行快速删除操作,因为使用迭代器从列表开头删除对象非常慢)

迭代器(抛出 ConcurrentModificationException):

import java.util.*;
import java.util.concurrent.*;

public class Main {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        List<Long> list = new LinkedList<>();
        for (long i1 = 0; i1 < 1_000_000; i1++) {
            list.add(i1);
        }

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        List<Callable<Integer>> tasks = new ArrayList<>();
        Iterator<Long> iterator = list.iterator();
        for (int i = 0; i < 10; i++) {
            tasks.add(() -> {
                int count = 0;
                while (iterator.hasNext()) {
                    iterator.next();
                    iterator.remove();
                    count++;
                }
                System.out.println("Thread " + Thread.currentThread() + " removed " + count + " items");
                return count;
            });
        }
        List<Future<Integer>> results = executorService.invokeAll(tasks);
        int sum = 0;
        for (Future<Integer> result : results) {
            sum += result.get();
        }
        System.out.println("sum of removed items = " + sum);
        executorService.shutdown();
    }

}

用 for i 循环替换(抛出 IndexOutOfBoundsException):


        List<Long> list = new ArrayList<>();
        for (long i1 = 0; i1 < 1_000_000; i1++) {
            list.add(i1);
        }

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        List<Callable<Integer>> tasks = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            tasks.add(() -> {
                int count = 0;
                for (int j = 0; j < list.size(); j++) {
                    list.remove(list.size() - 1);
                    count++;
                }
                System.out.println("Thread " + Thread.currentThread() + " removed " + count + " items");
                return count;
            });
        }
        List<Future<Integer>> results = executorService.invokeAll(tasks);
        int sum = 0;
        for (Future<Integer> result : results) {
            sum += result.get();
        }
        System.out.println("sum of removed items = " + sum);
        executorService.shutdown();

要避免这些异常,您可以做的一件事是使用 synchronized 块,它确保一次只允许在内部执行 1 个线程 - 进入后需要再次检查同步阻塞条件是否仍然为真:

        List<Long> list = new LinkedList<>();
        for (long i1 = 0; i1 < 1_000_000; i1++) {
            list.add(i1);
        }

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        List<Callable<Integer>> tasks = new ArrayList<>();
        Iterator<Long> iterator = list.iterator();
        Object lock = new Object();
        for (int i = 0; i < 10; i++) {
            tasks.add(() -> {
                int count = 0;
                while (iterator.hasNext()) {
                    synchronized (lock) {
                        if (iterator.hasNext()) {
                            iterator.next();
                            iterator.remove();
                            count++;
                        }
                    }
                }
                System.out.println("Thread " + Thread.currentThread() + " removed " + count + " items");
                return count;
            });
        }
        List<Future<Integer>> results = executorService.invokeAll(tasks);
        int sum = 0;
        for (Future<Integer> result : results) {
            sum += result.get();
        }
        System.out.println("sum of removed items = " + sum);
        executorService.shutdown();

Java 还具有并发集合,您可以使用这些集合来启用多个线程进行并行计算。

为每个线程提供自己的迭代器需要更改为 ConcurrentLinkedQueue 之类的内容,以避免并发修改异常。但是,代码给出了一个非常错误的结果:

        Collection<Long> list = new ConcurrentLinkedQueue<>();
        for (long i1 = 0; i1 < 1_000_000; i1++) {
            list.add(i1);
        }

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        List<Callable<Integer>> tasks = new ArrayList<>();
        Object lock = new Object();
        for (int i = 0; i < 10; i++) {
            tasks.add(() -> {
                int count = 0;
                Iterator<Long> iterator = list.iterator();
                while (iterator.hasNext()) {
                    synchronized (lock) {
                        if (iterator.hasNext()) {
                            iterator.next();
                            iterator.remove();
                            count++;
                        }
                    }
                }
                System.out.println("Thread " + Thread.currentThread() + " removed " + count + " items");
                return count;
            });
        }
        List<Future<Integer>> results = executorService.invokeAll(tasks);
        int sum = 0;
        for (Future<Integer> result : results) {
            sum += result.get();
        }
        System.out.println("sum of removed items = " + sum);
        executorService.shutdown();

打印出来的

sum of removed items = 1105846

所以看起来迭代器不是很线程安全:)

如果我们去掉迭代器:


        Queue<Long> list = new ConcurrentLinkedQueue<>();
        for (long i1 = 0; i1 < 1_000_000; i1++) {
            list.add(i1);
        }

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        List<Callable<Integer>> tasks = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            tasks.add(() -> {
                int count = 0;
                while (!list.isEmpty()) {
                    list.poll();
                    count++;
                }
                System.out.println("Thread " + Thread.currentThread() + " removed " + count + " items");
                return count;
            });
        }
        List<Future<Integer>> results = executorService.invokeAll(tasks);
        int sum = 0;
        for (Future<Integer> result : results) {
            sum += result.get();
        }
        System.out.println("sum of removed items = " + sum);
        executorService.shutdown();

这样更好:

sum of removed items = 1000007

放回锁:

        Queue<Long> list = new ConcurrentLinkedQueue<>();
        for (long i1 = 0; i1 < 1_000_000; i1++) {
            list.add(i1);
        }

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        List<Callable<Integer>> tasks = new ArrayList<>();
        Object lock = new Object();
        for (int i = 0; i < 10; i++) {
            tasks.add(() -> {
                int count = 0;
                while (!list.isEmpty()) {
                    synchronized (lock) {
                        if (!list.isEmpty()) {
                            list.poll();
                            count++;
                        }
                    }
                }
                System.out.println("Thread " + Thread.currentThread() + " removed " + count + " items");
                return count;
            });
        }
        List<Future<Integer>> results = executorService.invokeAll(tasks);
        int sum = 0;
        for (Future<Integer> result : results) {
            sum += result.get();
        }
        System.out.println("sum of removed items = " + sum);
        executorService.shutdown();

似乎有效

sum of removed items = 1000000

【讨论】:

  • 很好的解释。根据它,无法理解为什么我的代码会导致运行时异常(我已将代码添加到帖子中)
  • 你正在做我提到的事情。您在 list 上调用 remove,这会更改 list 的 modCount,而 iterator 的 expectedModCount 不会更改。
  • 基本上应该调用 iterator.remove(),而不是 list.remove
  • 我明白了。请只是一个小问题.. 如果我有两个不同的线程在同一个列表上运行 for/while 循环(而不是使用迭代器)。其中一个人删除了一个项目,在这种情况下不会发生异常?只有在使用 Iterators 系统时才“安全”?
  • 是的,看看 10 个迭代器会发生什么很有趣,我将结果添加到答案中。
猜你喜欢
  • 1970-01-01
  • 2018-10-21
  • 1970-01-01
  • 1970-01-01
  • 2020-03-17
  • 2018-06-27
  • 2015-03-24
  • 2019-09-07
  • 2020-05-27
相关资源
最近更新 更多