为什么ArrayList在多个线程修改时不会抛出ConcurrentModificationException异常?

3

ConcurrentModificationException:当对象的并发修改不被允许时,可能会抛出该异常。

上述是来自javadoc的ConcurrentModificationException定义。

因此,我尝试测试以下代码:

final List<String> tickets = new ArrayList<String>(100000);
for (int i = 0; i < 100000; i++) {
    tickets.add("ticket NO," + i);
}
for (int i = 0; i < 10; i++) {
    Thread salethread = new Thread() {
        public void run() {
            while (tickets.size() > 0) {
                tickets.remove(0);
                System.out.println(Thread.currentThread().getId()+"Remove 0");
            }
        }
    };
    salethread.start();
}

这段代码很简单。 10个线程从ArrayList对象中删除元素。 虽然多个线程访问同一个对象,但它可以正常运行,没有抛出异常。 为什么呢?

5个回答

7
我为您引用了大段ArrayList Javadoc,与您的利益相关的部分已经被突出显示。请注意,此实现未同步。如果多个线程同时访问一个 ArrayList 实例,并且至少有一个线程对列表进行结构性修改,则必须从外部同步。(结构性修改是添加或删除一个或多个元素或显式调整支持数组的大小的任何操作;仅仅设置元素的值不是结构性修改。)这通常通过在自然封装列表的某些对象上进行同步来完成。如果不存在这样的对象,则应使用 Collections.synchronizedList 方法“包装”列表。最好在创建时执行此操作,以防止意外地未同步访问列表,例如:List list = Collections.synchronizedList(new ArrayList(...)); 此类的 iterator 和 listIterator 方法返回的迭代器是快速失败的:如果在迭代器创建之后的任何时候以任何方式(除迭代器自己的 remove 或 add 方法之外)对列表进行结构性修改,迭代器将抛出 ConcurrentModificationException。因此,在面对并发修改时,迭代器会快速而干净地失败,而不是冒着在未来的某个不确定时间内出现任意的、非确定性的行为的风险。 请注意,不能保证迭代器的快速失败行为,因为一般来说,在未同步并发修改的情况下无法做出任何硬性保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常正确性的程序是错误的:应仅使用迭代器的快速失败行为来检测 bug。 如果通过迭代器访问列表时在结构上修改了它,ArrayList 通常会抛出并发修改异常(但即使如此也不能绝对保证)。请注意,在您的示例中,您直接从列表中删除元素,而没有使用迭代器。 如果您感兴趣,您还可以浏览 ArrayList.remove 的实现,以更好地了解其工作原理。

3
我认为在这种情况下,“concurrent”并不一定意味着与线程相关,或者至少不一定是这个意思。通常,在迭代集合时修改它会导致“ConcurrentModificationException”异常。请注意保留HTML标签。
List<String> list = new ArrayList<String>();
for(String s : list)
{
     //modifying list results in ConcurrentModificationException
     list.add("don't do this");     

}

请注意,Iterator<>类有一些可以规避此问题的方法:
for(Iterator it = list.iterator(); it.hasNext())
{
     //no ConcurrentModificationException
     it.remove(); 
}

简短明了。并发不一定意味着并行(正如你的回答所说明的那样)。 - John Vint

1
因为您没有使用迭代器,所以不会出现ConcurrentModificationException异常。
调用remove(0)将简单地删除第一个元素。如果另一个线程在执行完成之前删除了0,则可能不是调用者所预期的相同元素。

1
它运行得很好,没有抛出异常。为什么呢?
这是因为并发修改是允许的。
异常的描述如下:
“当检测到对象的并发修改不被允许时,此异常可能会被方法抛出。”
明显的暗示是有(或可能有)允许的并发修改。实际上,对于标准的Java非并发集合类,允许并发修改……只要它们不在迭代期间发生即可。
这样做的原因是,对于非并发集合,迭代时进行修改基本上是不安全和不可预测的。即使您正确地进行了同步(这并不容易1),结果仍然是不可预测的。在常规集合类中包含用于并发修改的“快速失败”检查,因为这是使用Java 1.1集合类的多线程应用程序中常见的Heisenbugs来源。
例如,“synchronizedXxx”包装器类不会也不能与迭代器同步。问题在于迭代涉及交替调用next()hasNext(),而要排除其他线程执行一对方法调用的唯一方法是使用外部同步。在Java中,包装器方法并不实用。

1
你没有收到ConcurrentModificationException异常的原因是ArrayList.remove方法不会抛出该异常。你可以通过启动另一个线程来遍历数组来获得这个异常:
final List<String> tickets = new ArrayList<String>(100000);
for (int i = 0; i < 100000; i++) {
    tickets.add("ticket NO," + i);
}
for (int i = 0; i < 10; i++) {
    Thread salethread = new Thread() {
        public void run() {
            while (tickets.size() > 0) {
                tickets.remove(0);
                System.out.println(Thread.currentThread().getId()+"Remove 0");
            }
        }
    };
    salethread.start();
}
new Thread() {
    public void run() {
        int totalLength = 0;
        for (String s : tickets) {
            totalLength += s.length();
        }
    }
}.start();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接