java—为什么在运行列表时通过迭代器删除项目时引发异常?

iezvtpos  于 2021-07-06  发布在  Java
关注(0)|答案(1)|浏览(314)

我查了listiterator类的remove方法的代码,不明白为什么在运行列表时删除了一个项之后,在试图获取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();
                }
            }

我不明白的是:
为什么remove方法获取lastret而不是cursor。我们要删除当前项而不是已传递的项?
2.为什么lastret设置为-1?
3.下一行:expectedmodcount=arraylist.this.modcount;将expectedmodcount设置为等于modcount,这样在下一次迭代中,当检查两个变量是否相等时,if将“say”true,一切正常。我在网上读了很多文章,也有一些答案,但还是听不懂
因为我得到的响应是代码不会导致运行时异常,下面是导致异常的代码:

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);
            }
        }
}
qkf9rpyu

qkf9rpyu1#

iterator.remove()的javadoc说明:

Removes from the underlying collection the last element returned by this iterator

因此,在调用此方法时,光标指向下一个元素,因此使用lastret。
-1表示未找到。它被设置为-1,因为您刚刚删除了lastret,因此它不再存在。这意味着如果不先调用next(),就不能再次调用remove():如果查看next(),它将更新lastret
调用arraylist.this.remove()更改modcount(modcount由abstractlist管理,abstractlist是您正在查看的arraylist迭代器的一个超类)。因为这个更改是由方法本身引起的,所以我们知道这是一个有效的更改(换句话说,我们只是请求更改列表)。因此局部变量(expectedmodcount)被更新。如果这个列表被迭代器的另一个示例修改,那么abstractlist的modcount将改变,但是属于当前示例的expectedmodcount不会改变。这样,当前示例将知道存在并发修改。
额外问题:
我理解。请回答一个小问题。。如果有两个不同的线程在同一个列表上运行for/while循环(而不是迭代器)。其中一个正在删除一个项目,在这种情况下不会发生异常?只有在使用迭代器时系统才是“安全的”?
多线程不是一个“小”问题:)
简而言之,不,这两种情况都不安全。无论使用什么循环,当一个线程正在检查是否可以安全地执行另一个循环时(无论是.hasnext()还是i<size()),另一个线程可以同时删除/添加元素。因此,如果使用for循环,则不会出现并发修改异常,但可能会出现索引越界异常。
你应该自己试试。下面是一些黑客示例(我使用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();

要避免这些异常,可以做的一件事是使用同步块,它确保一次只允许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

相关问题