并发修改异常:向ArrayList添加元素

66

问题出现在

Element element = it.next();

这行代码所在的代码块是在一个 OnTouchEvent 内部

for (Iterator<Element> it = mElements.iterator(); it.hasNext();){
    Element element = it.next();

    if(touchX > element.mX  && touchX < element.mX + element.mBitmap.getWidth() && touchY > element.mY   
            && touchY < element.mY + element.mBitmap.getHeight()) {  

        //irrelevant stuff..

        if(element.cFlag){
            mElements.add(new Element("crack",getResources(), (int)touchX,(int)touchY));
            element.cFlag = false;

        }           
    }
}
所有这些代码都位于 synchronized(mElements) 中,其中 mElements 是一个 ArrayList<Element>
当我触摸一个 Element 时,它可能会激活 cFlag,这将创建另一个具有不同属性的 Element,该元素将在不到一秒钟的时间内从屏幕上掉落并自行销毁。 这是我创建粒子效果的方式。我们可以称之为 "particle" crack,就像构造函数中的字符串参数一样。
这一切都很好,直到我添加了另一个主要的 Element。现在我同时在屏幕上有两个 Element,如果我点击最新的 Element,它能正常工作并启动粒子。
但是,如果我点击并激活较旧的 Element 上的 cFlag,那么它就会抛出异常。
 07-28 15:36:59.815: ERROR/AndroidRuntime(4026): FATAL EXCEPTION: main
07-28 15:36:59.815: ERROR/AndroidRuntime(4026): java.util.ConcurrentModificationException
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at java.util.ArrayList$ArrayListIterator.next(ArrayList.java:573)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at com.Juggle2.Panel.onTouchEvent(Panel.java:823)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at android.view.View.dispatchTouchEvent(View.java:3766)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:863)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:863)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:1767)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1119)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at android.app.Activity.dispatchTouchEvent(Activity.java:2086)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:1751)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1785)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at android.os.Handler.dispatchMessage(Handler.java:99)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at android.os.Looper.loop(Looper.java:123)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at android.app.ActivityThread.main(ActivityThread.java:4627)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at java.lang.reflect.Method.invokeNative(Native Method)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at java.lang.reflect.Method.invoke(Method.java:521)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:893)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:651)
07-28 15:36:59.815: ERROR/AndroidRuntime(4026):     at dalvik.system.NativeStart.main(Native Method)

我该如何让这个工作?


可能是Concurrent Modification exception的重复问题。 - fabian
11个回答

90

ConcurrentModificationException 出现在使用 Iterator 遍历列表时,同时对列表进行修改(添加或删除元素)。

尝试使用:

List<Element> thingsToBeAdd = new ArrayList<Element>();
for(Iterator<Element> it = mElements.iterator(); it.hasNext();) {
    Element element = it.next();
    if(...) {  
        //irrelevant stuff..
        if(element.cFlag){
            // mElements.add(new Element("crack",getResources(), (int)touchX,(int)touchY));
            thingsToBeAdd.add(new Element("crack",getResources(), (int)touchX,(int)touchY));
            element.cFlag = false;
        }           
    }
}
mElements.addAll(thingsToBeAdd );

正如Jon所建议的那样,您应该考虑使用加强型for循环。


1
当我需要使用从列表中删除时,传统的for循环无法正常工作,我不得不每次都进行修改以适应循环中的“i”。 - Amit Tumkur
2
@AmitTumkur 尝试使用 "iterator.remove();" 而不是 "list.remove(object)" :) - Andrii Kovalchuk

76

我通常使用像这样的东西:

for (Element element : new ArrayList<Element>(mElements)) {
    ...
}

快速、干净且无错误

另一个选择是使用CopyOnWriteArrayList


但是新的ArrayList对象将执行列表的浅拷贝,指向同一个列表。 - Ninad Kambli
7
不会指向同一个列表,而是复制了这个列表。 - konmik
但是这样不会遍历一个具有不同数量项的列表吗?如果您从原始列表中删除一个项目,它仍然存在于复制的ArrayList中,对吗? - Opiatefuchs
修改后的列表中的项目数量显然会有所不同。关键是,您仍然可以在修改列表时遍历所有原始列表项。 - konmik

22

当您迭代集合时,不允许向其添加条目。

一种解决方法是在您迭代 mElements 时创建一个新的 List<Element> 来存储新条目,然后在迭代完成后将所有新条目添加到 mElements 中 (mElements.addAll(newElements))。当然,这意味着您将无法为这些新元素执行循环体-这会成为问题吗?

同时,我建议更新代码以使用增强型for循环

for (Element element : mElements) {
    ...
}

实际上,我一开始使用了增强型for循环。最近我才使用迭代器,因为我认为这可以解决问题,但事实并非如此。 - user773737
3
我认为增强型for循环在后台使用迭代器,因此使用它可能会导致相同的问题。 - ty1824
2
@OWiz:这两种形式编译后基本上会生成相同的代码。 - Jon Skeet

16
一个带索引的for循环也应该有效。
for (int i = 0; i < collection.size(); i++)

8
你可能会陷入无限循环。 - user802421

3

使用迭代器也可以解决并发问题,例如:

Iterator<Object> it = iterator.next().iterator();
while (it.hasNext()) {
    it.remove();
}

7
如果你想要移除元素,迭代器会很有用,但如果你想像这个问题所问的那样向列表中添加元素,迭代器就没有用了。 - Robert

2
在这种情况下,从列表中添加会导致CME,无论使用多少次synchronized都无法避免。相反,考虑使用迭代器进行添加...
        for(ListIterator<Element> it = mElements.listIterator(); it.hasNext();){
            Element element = it.next();

            if(touchX > element.mX  && touchX < element.mX + element.mBitmap.getWidth() && touchY > element.mY   
                    && touchY < element.mY + element.mBitmap.getHeight()) {  

                //irrelevant stuff..

                if(element.cFlag){
                    // mElements.add(new Element("crack",getResources(), (int)touchX,(int)touchY));
                    it.add(new Element("crack",getResources(), (int)touchX,(int)touchY));
                    element.cFlag = false;

                }           
            }
        }

我认为这样说有些含糊不清:...The problem occurs at Element element = it.next(); 为了更加准确,需要注意上面的内容不能保证完全正确。 API文档指出:...在未同步并发修改的情况下,无法做出任何硬性保证。快速失败操作尽最大努力抛出ConcurrentModificationException异常...

2
你可以使用自动递减的for循环,下次再处理额外的元素。最初的回答。
List additionalElements = new ArrayList();
for(int i = mElements.size() - 1; i > -1 ; i--){
    //your business
    additionalElements.add(newElement);
}
mElements.add(additionalElements);

2

我创建了一个锁(Kotlin)来解决问题:

最初的回答:

import java.util.concurrent.locks.ReentrantLock

Class A {
    private val listLock = ReentrantLock()
    fun doSomething(newElement){
        listLock.lock()
        list.add(newElement)
        listLock.unlock()
    }
}

1

我在适配器中遍历列表时尝试了所有可能,但由于反复打击,它显示了异常被抛出的消息。我尝试将列表强制转换为

 = (CopyOnWriteArraylist<MyClass>)mylist.value;

但它也抛出了一个CouldNotCastException异常,(最终我思考了一下为什么他们使用或提供我们一个强制转换的设施)。
我甚至使用了所谓的同步块,但它也没有起作用,或者我可能使用方法不正确。
因此,当我最终使用try catch块中的#all of time#异常处理技术时,它起作用了。 所以将你的代码放在其中。
try{
//block

}catch(ConcurrentModificationException){
//thus handling my code over here
}

1

通常情况下,接受的解决方案(创建集合的副本)效果良好

然而,如果Element 包含另一个集合,这并不会进行深度复制

例如:

class Element {
   List<Kid> kids;

   getKids() {
      return kids;
   }
}

现在,当您创建元素列表的副本时:
for (Element element : new ArrayList<Element>(elements)) { ... }

您仍然可能会在迭代element.getKids()并且同时修改该元素的kids时,遇到ConcurrentModificationException异常。

回头看很明显,但我最终进入了这个线程,因此也许这个提示可以帮助其他人:

class Element {
   List<Kid> kids;

   getKids() {
      // Return a copy of the child collection
      return new ArrayList<Kid>(kids);
   }
}

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