使用复制构造函数时同时修改列表

时间:2012-06-14 14:01:01

标签: java concurrency concurrentmodification

以下代码是否会导致ConcurrentModificationException或其他副作用?

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

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

3 个答案:

答案 0 :(得分:8)

修改

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

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

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


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

答案 1 :(得分:1)

你需要考虑一下你在这做什么。如果list的类不是线程安全的,那么您可以使用此代码以及list完全销毁newList - CME将是您遇到的最少问题。 (我建议一个不会抛出CME的类,但在这种情况下,CME是好的的东西。)另请注意:此代码很难测试。你会在每次失败之间得到零到十亿无问题的运行,并且失败可能非常微妙,尽管它们更可能是巨大的并且超出理性解释。

最快的解决方法是锁定list。你想确保把它锁定无处不在它被使用;你并没有真正锁定列表,而是锁定了你正在访问它的代码块。您必须锁定所有访问权限。缺点是在创建新列表时您将阻止其他线程。这真的是要走的路。但是,正如你所说,如果“列表非常庞大”,你可能会担心表现,所以我会继续......

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

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

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

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

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

阵列是最快的事情,但你需要谨慎使用大型阵列。注意每个分配和免费。给他们一个合适的初始大小,这样他们就不会重新分配空间。 (假装你是C程序员。)善待你的GC。如果你必须无所畏惧地创建和释放大型列表,请考虑使用链接类:LinkedList,TreeMap,ConcurrentLinkedQueue等。它们只使用一点点内存,GC喜欢它们。

答案 2 :(得分:0)

我创建了一些代码来测试@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));
    }
}