2011-06-09 65 views
1

我目前正试图学习如何正确处理对Collections的多线程访问,所以我写了下面的Java应用程序。又一个ConcurrentModificationException问题

正如你所看到的,我创建了一个同步的ArrayList,我尝试从一个线程中访问一次,一次没有。

我使用for循环遍历ArrayList。为了防止同时在List上多次访问,我将循环封装到一个同步块中。

public class ThreadTest { 

    Collection<Integer> data = Collections.synchronizedList(new ArrayList<Integer>()); 
    final int   MAX  = 999; 

    /** 
    * Default constructor 
    */ 
    public ThreadTest() { 
     initData(); 
     startThread(); 
     startCollectionWork(); 
    } 

    private int getRandom() { 
     Random randomGenerator = new Random(); 
     return randomGenerator.nextInt(100); 
    } 

    private void initData() { 
     for (int i = 0; i < MAX; i++) { 
      data.add(getRandom()); 
     } 
    } 

    private void startCollectionWork() { 
     System.out.println("\nStarting to work on data outside of thread"); 
     synchronized (data) { 
      System.out.println("\nEntered synchronized block outside of thread"); 
      for (int value : data) { // ConcurrentModificationException here! 
       if (value % 5 == 1) { 
        System.out.println(value); 
        data.remove(value); 
        data.add(value + 1); 
       } else { 
        System.out.println("value % 5 = " + value % 5); 
       } 
      } 
     } 
     System.out.println("Done working on data outside of thread"); 
    } 

    private void startThread() { 
     Thread thread = new Thread() { 
      @Override 
      public void run() { 
       System.out.println("\nStarting to work on data in a new thread"); 
       synchronized (data) { 
        System.out.println("\nEntered synchronized block in thread"); 
        for (int value : data) { // ConcurrentModificationException 
         if (value % 5 == 1) { 
          System.out.println(value); 
          data.remove(value); 
          data.add(value + 1); 
         } else { 
          System.out.println("value % 5 = " + value % 5); 
         } 
        } 
       } 
       System.out.println("Done working on data in a new thread"); 
      } 
     }; 
     thread.start(); 
    } 
} 

但是每次for循环都进入,我得到一个ConcurrentModificationException异常。这是我的控制台输出(每改变一次都会改变):

Starting to work on data outside of thread 

Entered synchronized block outside of thread 
51 

Starting to work on data in a new thread 

Entered synchronized block in thread 
value % 5 = 2 
value % 5 = 2 
value % 5 = 4 
value % 5 = 3 
value % 5 = 2 
value % 5 = 2 
value % 5 = 0 
21 
Exception in thread "main" java.util.ConcurrentModificationException 
    at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372) 
    at java.util.AbstractList$Itr.next(AbstractList.java:343) 
    at ThreadTest.startCollectionWork(ThreadTest.java:50) 
    at ThreadTest.<init>(ThreadTest.java:32) 
    at MultiThreadingTest.main(MultiThreadingTest.java:18) 
Exception in thread "Thread-1" java.util.ConcurrentModificationException 
    at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372) 
    at java.util.AbstractList$Itr.next(AbstractList.java:343) 
    at ThreadTest$1.run(ThreadTest.java:70) 

怎么了?

Ps:请不要只发布链接到多线程的方法,因为我已经阅读了足够多。我只是好奇为什么我的应用程序没有按照我的要求运行。


更新:我取代了对(X:Y)的语法具有显式迭代器和一个while循环。这个问题仍然是虽然..

synchronized(data){ 
     Iterator<Integer> i = data.iterator(); 
     while (i.hasNext()) { 
     int value = i.next(); // ConcurrentModificationException here! 
     if (value % 5 == 1) { 
      System.out.println(value); 
      i.remove(); 
      data.add(value + 1); 
     } else { 
      System.out.println("value % 5 = " + value % 5); 
     } 
    } 
} 

回答

2

一旦你迭代了一个集合,你就有了迭代代码块和集合之间的契约,因为它在那个时候存在。合同基本上规定,您将按迭代顺序一次获取集合中的每个项目。

问题是,如果您在迭代某项时修改集合,则无法维护该合同。集合中的删除操作将从集合中删除该元素,并且该元素可能需要在初始迭代中出现才能满足合同。如果可以通过在集合中存在的元素之前开始的迭代检测元素,则集合中的插入同样会出现问题。

虽然使用多个线程更容易分解此合约,但您可以使用单个线程(如果您选择这样做)分解合同。

这通常是如何实现的:集合包含一个“修订版本号”,并且在迭代器获取集合中的“next”元素之前,它检查集合的修订版本号是否仍然相同当迭代器启动时。这只是实现它的一种方式,还有其他的。

所以,如果你想迭代你可能想要改变的东西,一个合适的技术是创建一个集合的副本并迭代该副本。这样,您可以修改原始集合,但不会更改您计划处理的项目的数量,位置和存在。是的,还有其他技术,但从概念上说,它们都属于“保护你在迭代的副本,同时更改迭代器无法访问的其他东西”。

+0

很好的答案!非常感谢。在同一时间迭代副本并修改原始集合解决了问题! – Timo 2011-06-09 14:18:13

+1

有一个角落案件可以移除。一些迭代器实现'iterator.remove()'操作,它允许迭代器(每个元素只有一次)移除当前元素。由于迭代器知道它已经呈现了当前元素,因此它不会担心集合修改。但要注意的一点是,'iterator.remove()'是迭代器的可选操作。换句话说,一些迭代器(对于特定的集合)每次调用'iterator.remove()'时都会抛出'OperationNotSupported'异常。 – 2011-06-09 14:24:36

+0

另外,调用一个迭代器的remove方法仍然可能导致另一个打开的迭代器抛出ConcurrentModificationException,因此remove方法对异常并不完全安全。 – Bhaskar 2011-06-09 14:29:48

2

增强的for循环

for (int value : data) { 

使用Java迭代器盖下。迭代器是快速失败的,所以如果在迭代器处于活动状态时底层集合被修改(即通过移除一个元素),您将获得异常。在这里你的代码会导致这种变化的底层集合:

data.remove(value); 
data.add(value + 1); 

更改代码中使用java.util.Iterator明确,并使用其remove方法。如果您需要在迭代时向Collection中添加元素,则可能需要查看来自java.util.concurrent包的合适数据结构,例如, BlockingQueue,在那里你可以调用它的take方法,它会阻塞,直到有数据存在;但可以通过offer方法添加新对象(非常简单的概述 - 谷歌更多)

+0

我用while循环替换了for(x:y)语法,并调用显式迭代器(请参阅更新后的问题的底部),但问题仍然存在。所以,我想如何检索迭代器并不重要? – Timo 2011-06-09 14:09:33

1

如果您正在遍历集合,则只允许通过迭代器从列表中删除项目,因此您可以获得ConcurrentModificationException只有一个线程。

更新的问题后的更新回复: 您在迭代时不允许向列表中添加元素。

+0

我更新了我的代码(请参阅我的原始问题的底部),只通过迭代器删除项目。但是,异常会被抛出.. – Timo 2011-06-09 14:10:45

+0

@valmar,您不能在迭代时向列表中添加元素。 – Kaj 2011-06-09 14:12:36

2

ConcurrentModificationException出现,因为你对矫正列表而迭代它...它无关,在这种情况下,多线程...

for (int value : data) { // ConcurrentModificationException here! 
       if (value % 5 == 1) { 
        System.out.println(value); 
        data.remove(value); // you cannot do this 
        data.add(value + 1); // or that 
       } else { 
        System.out.println("value % 5 = " + value % 5); 
       } 
+0

恩,所以如果我遍历一个集合,并想删除一个特定的对象(当一个条件x等于true),我不能这样做?我怎么能解决这个问题? – Timo 2011-06-09 14:07:27

+0

个人我保存我想添加/删除在另一个列表中的项目,并在迭代完成后保存/删除它们 – 2011-06-09 14:22:37

1

的问题不是多线程的,因为,你做的如果安全。但仅仅是因为您在迭代时修改了集合。

0

正如已经指出的那样,您在迭代它时修改了Collection,这会导致您的问题。

数据更改为List,然后使用常规for循环来遍历它。那么你将不再有迭代器来处理,从而消除你的问题。

List<Integer> data =... 

for (int i=0; i<data.size(); i++) { 
     int value = data.get(i); 

     if (value % 5 == 1) { 
      System.out.println(value); 
      i.remove(); 
      data.add(value + 1); 
     } else { 
      System.out.println("value % 5 = " + value % 5); 
     } 
} 
0

ConcurrentModificationException并未通过在迭代完成的集合周围使用同步块来消除。在以下步骤序列中发生异常:

  1. 从collecion(通过调用其迭代器方法或for循环结构)获取迭代器。
  2. 开始迭代(通过调用next()或在for循环中)
  3. 让集合被修改(通过除迭代器的方法以外的任何方式)(在同一个线程或不同的线程中:这就是发生的事情在你的代码中)。请注意,此修改可能以线程安全的方式发生,即按顺序或依次 - 无关紧要 - 它仍将导致CME)
  4. 使用之前获得的相同迭代器继续迭代(在步骤3中修改之前)

为了避免发生异常,必须确保在两个线程中的任何一个启动for循环之前,不要修改集合,直到循环结束。