有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java在使用复制构造函数时并发修改列表

以下代码会导致ConcurrentModificationException或其他副作用吗

ArrayList<String> newList = new ArrayList<String>(list);

考虑到列表的大小非常大,并且在执行上述代码时,另一个线程正在同时修改列表


共 (3) 个答案

  1. # 1 楼答案

    我创建了一些代码来测试@Gray说的话。从数组列表中删除时使用复制构造函数会导致创建的列表中出现空元素。您可以看到,在以下代码段中,错误条目的数量不断增加:

    public static void main(String[] args) {
    
        final int n = 1000000;
        final int m = 100000;
        final ArrayList<String> strings = new ArrayList<String>(n);
    
        for(int i=0; i<n; i++) {
            strings.add(new String("abc"));
        }
    
    
        Thread creatorThread = new Thread(new Runnable() {
            @Override
            public void run() {
                ArrayList<String> stringsCme = new ArrayList<String>(strings);
                int wrongEntries = 0;
                for(int i=0; i<m; i++) {
                    stringsCme = new ArrayList<String>(strings);
    
                    for(String s : stringsCme) {
                        if(s == null || !s.equals("abc")) {
                            //System.out.println("Wrong entry: " + s);
                            wrongEntries++;
                        }
                    }
    
                    if(i % 100 == 0)
                        System.out.println("i = " + i + "\t list: " + stringsCme.size() + ", #wrong entries: " + wrongEntries);
                }
    
                System.out.println("#Wrong entries: " + wrongEntries);
            }
        });
        creatorThread.start();
    
        for(int i=0; i<m; i++) {
            strings.remove(MathUtils.random(strings.size()-1));
        }
    }
    
  2. # 2 楼答案

    编辑:

    我最初的回答是肯定的,但正如@JohnVint正确指出的那样,它不会是ConcurrentModificationException,因为在幕后ArrayList使用System.arrayCopy(...)复制数组。请参阅末尾的代码片段

    问题是,在执行此复制时,另一个线程正在对元素数组进行更改。您可能会得到IndexOutOfBoundsException、未初始化的数组值,甚至是某种本机内存访问异常,因为System.arraycopy(...)是在本机代码中完成的

    在更新和复制期间,您需要在列表上同步,以防止这些竞争条件,并建立内存屏障,以确保支持ArrayList的元素数组适当地是最新的


    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        ...
    }
    
    // ArrayList
    public Object[] toArray() {
            return Arrays.copyOf(elementData, size);
    }
    
    // Arrays
    public static <T,U> T[] copyOf(U[] original, int newLength,
        Class<? extends T[]> newType) {
        ...
        System.arraycopy(original, 0, copy, 0,
            Math.min(original.length, newLength));
    }
    
    // System
    public static native void arraycopy(Object src,  int  srcPos,
        Object dest, int destPos, int length);
    
  3. # 3 楼答案

    你需要想想你在这里做什么。如果list的类不是线程安全的,您可以使用此代码完全销毁list,同时newListCME将是您的最小问题。(我建议一个不抛出CME的类,但在这种情况下,CME是一个好东西。)还要注意:这段代码很难测试。在每次失败之间,你会得到0到10亿次无问题运行,失败可能非常微妙,尽管它们更可能是巨大的,超出了理性的解释

    最快的修复方法是锁定list。您要确保在使用它的任何地方锁定它;您并没有真正锁定列表,而是锁定了从中访问它的代码块。您必须锁定所有访问。缺点是在创建新列表时会阻塞另一个线程。这才是真正的出路。然而,若你们说“名单非常庞大”,你们可能会担心性能,所以我会继续

    如果newList被视为不可变的,并且您在创建后经常使用它,那么这样做是值得的。许多代码现在可以同时读取newList,而不会出现问题,而不必担心不一致。但最初的创造仍然存在阻碍

    下一步是使list成为java。util。ConcurrentLinkedQueue。(如果您需要更高级的东西,可以同时使用地图和设置。)这个东西可以有一堆线程读取它,同时添加和删除更多的线程,并且它总是有效的。它可能不包含您认为它包含的内容,但迭代器不会进入无限循环(如果list是java.util.LinkedList,可能会发生这种情况)。这使得newList可以在一个内核上创建,而另一个线程可以在另一个内核上工作

    缺点:如果list是一个ArrayList,您可能会发现切换到并发类需要做一些工作。并发类使用更多内存,通常比ArrayList慢。更重要的是list的内容可能不一致。(实际上,您已经遇到了这个问题。)您可能会在另一个线程中同时添加或删除条目A和B,并期望这两个条目都在newList中,而实际上只有一个条目在newList中非常容易,迭代器在添加或删除一个条目之后,但在另一个条目之前通过。(单核机器没有这个问题。)但是如果list已经被认为处于一个恒定的、无序的通量中,这可能正是你想要的

    另一个不同的副作用:您必须小心使用大型数组和使用它们的东西(如ArrayList和HashTable)。当您删除条目时,它们不会占用更少的空间,因此最终会有一堆大数组占用大部分内存,其中的数据很少

    更糟糕的是,当您添加条目时,它们会释放旧数组并分配新的、更大的数组, 这会导致空闲内存的碎片化。也就是说,空闲内存大部分是从旧数组中删除的块,没有一个大到足以用于下一次分配。垃圾收集器将尝试对所有这些进行碎片整理,但这需要大量的工作,而且GC倾向于抛出内存不足的异常,而不是花时间重新排列空闲块,以便获得您刚刚请求的最大内存块。因此,当只有10%的内存在使用时,就会出现内存不足错误

    阵列是最快的东西,但您需要小心使用大型阵列。注意每次分配和免费。给它们一个合适的初始大小,这样它们就不会重新分配空间。(假装你是一名C程序员。)善待你的GC。如果你必须很好地创建和释放和调整大列表,请考虑使用链接类:链接表、树图、并发链接队列等等。它们只使用少量的内存,GC喜欢它们。p>