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

时间:2013-02-19 02:45:29

标签: java multithreading arraylist thread-safety

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 个答案:

答案 0 :(得分:7)

为了您的利益,我引用ArrayList Javadoc的大部分内容。解释您所看到的行为的相关部分将突出显示。

  

请注意,此实现未同步。如果有多个线程   同时访问一个ArrayList实例,并且至少有一个   线程在结构上修改列表,必须同步   外部即可。 (结构修改是添加或的任何操作   删除一个或多个元素,或显式调整后备数组的大小;   仅设置元素的值不是结构   修改。)这通常通过同步一些来完成   自然封装列表的对象。如果不存在这样的对象,   列表应该使用Collections.synchronizedList“包装”   方法。这最好在创建时完成,以防止意外   对列表的非同步访问:

     

List list = Collections.synchronizedList(new ArrayList(...));

     

此类的iterator和listIterator方法返回的迭代器   快速失败:如果列表在之后的任何时间进行结构修改   迭代器以任何方式创建,除非通过迭代器自己创建   删除或添加方法,迭代器将抛出一个   ConcurrentModificationException的。因此,面对并发   修改,迭代器快速而干净地失败,而不是   在不确定的时间冒着任意的,非确定性的行为   在将来。

     

请注意,无法保证迭代器的快速失败行为   一般来说,不可能做出任何硬性保证   存在未同步的并发修改。快速失败   迭代器抛出ConcurrentModificationException就是尽力而为   基础。因此,编写一个依赖的程序是错误的   关于它的正确性的这个例外:快速失败的行为   迭代器应该只用于检测错误。

如果在通过迭代器 访问列表时从结构上修改列表 ,则ArrayLists通常会抛出并发修改异常(但即使这不是绝对保证)。请注意,在您的示例中,您将直接从列表中删除元素,并且您没有使用迭代器。

如果它引起你的兴趣,你也可以浏览ArrayList.remove的实现,以便更好地理解它是如何工作的。

答案 1 :(得分:2)

我不认为'并发'在这种情况下意味着线程相关,或者至少它并不一定意味着。 ConcurrentModificationException通常来自于在迭代过程中修改集合。

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

答案 2 :(得分: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(); 。你可以通过启动一个遍历数组的附加线程来获得一个:

{{1}}

答案 3 :(得分:1)

因为您没有使用迭代器,所以不会抛出ConcurrentModificationException

调用remove(0)只会删除第一个元素。如果另一个线程在执行完成之前删除0,则它可能不是调用者所期望的相同的元素。

答案 4 :(得分:1)

  

但它运行正常。没有异常被抛出。为什么呢?

仅仅因为并发修改是允许的。

异常的描述说明了这一点:

  

“当不允许进行此类修改时,检测到并发修改对象的方法可能抛出此异常。

明确的含义是(或可能)允许的并发修改。事实上,对于标准的Java非并发集合类,允许并发修改......只要它们在迭代期间不会发生。


这背后的原因是,对于非并发集合,迭代时的修改从根本上说是不安全和不可预测的。即使您要正确同步(并且这不容易 1 ),结果仍然是不可预测的。并发修改的“快速失败”检查包含在常规集合类中,因为这是使用Java 1.1集合类的多线程应用程序中Heisenbugs的常见来源。

1-例如,“synchronizedXxx”包装类不会,也不能与迭代器同步。问题是迭代涉及交替调用next()hasNext(),并且在排除其他线程时执行一对方法调用的唯一方法是使用外部同步。包装器方法在Java中不实用。