即使列表已同步,为什么仍会出现ConcurrentModificationException?

15

我有一个Android多线程应用程序。

有一定的概率会出现两个或更多的触发器运行相同的代码部分。

我有一个对象列表。

我让它通过Collections.synchronizedList进行同步。

private List<WmGroupItemSample> mGroupItemSampleList;

mGroupItemSampleList = new ArrayList<WmGroupItemSample>();
mGroupItemSampleList = Collections.synchronizedList(mGroupItemSampleList);

然而,有时候我会在某一行上遇到异常:

Collections.sort(mGroupItemSampleList, new GroupItemSampleComparator());

java.util.ConcurrentModificationException
       at java.util.AbstractList$SimpleListIterator.next(AbstractList.java:62)
       at java.util.Collections.sort(Collections.java:1895)
  • 这个流程是否合法?
  • 我是否需要创建副本并在副本上运行排序?
  • Collections.synchronizedList 为什么不能防止这个异常?

[编辑]

GroupItemSampleComparator

public class GroupItemSampleComparator implements java.util.Comparator<WmGroupItemSample> {

    public GroupItemSampleComparator() {
        super();        
    }

    public int compare(WmGroupItemSample s1, WmGroupItemSample s2) {
       return ( (s2.getStartDate() - s1.getStartDate()) > 0 ) ? (-1) : (1);
    }
}

谢谢,


11
这个异常可以在没有任何[额外]线程的情况下重现 - 因此,同步无关紧要。 - user2864740
1
你使用的是哪个Java版本?我查看了一些Collections.java,但第1895行并不接近sort。 - laune
1
大家,为什么要专注于比较器呢? - Pavel Horal
1
@pavel:我记得最初的问题是没有多个线程参与的。这就是为什么每个人都要求使用比较器。阅读编辑后的版本,似乎这只是“正常”的并发修改。 - Scheintod
显示剩余14条评论
4个回答

24

基本问题在于同步列表的同步方式不够有用。

问题在于虽然它的方法是同步的,但是像移动元素这样应该是原子操作的动作却不是,因为移动所需的单独调用没有一起被同步。这意味着其他线程可以在单个方法调用之间进入。因此,同步集合现在已经基本过时了。

尽管存在这个缺陷,如果你的另一个线程在你的线程进行排序时添加一个元素,你将会得到这个异常,因为排序会迭代,而在迭代期间更改列表会导致异常。

幸运的是,JDK有新的Collection类,具有工业强度(和有用)的同步性,这得益于java.util.concurrent包。

CopyOnWriteArrayList替换您的列表,不要“同步”它,您就可以放心使用了。


你知道在这种情况下我不能使用Collections.sort - snaggs
它能够工作,但问题是“迭代器不会反映自创建迭代器以来列表的添加、删除或更改”。 - NullPointerException
"需要单独调用移动,而这些调用不会同步在一起" - 您可以通过在直接锁定列表的块上调用它们来将多个方法调用同步在一起。以下代码在强制其他线程等待排序完成后再修改列表时对列表进行排序:synchronized (mGroupItemSampleList) { Collections.sort(mGroupItemSampleList, new GroupItemSampleComparator()); }" - Jack
@Jack 但这正是问题所在。如果你有一个“同步列表”,你不希望自己去同步它!因此,人们不会去同步,从而导致不可避免的错误。 - Bohemian
@Bohemian同意,我只是想提一下,在同步列表上执行多个操作是可以原子化的。所写的答案暗示了这是不可能的,而唯一的选择是使用不同的实现。 - Jack

9

Collections.synchronizedList(list) 返回一个同步的列表,这意味着该列表的方法将会被同步执行(仅有一个方法可以在同一时间运行)。

然而,这并意味着当有其他人(或可能是你自己)正在使用其迭代器迭代列表时,您就不能调用列表的方法(由iterator()返回的迭代器不是同步的)。synchronizedList() 不能保护您免受ConcurrentModificationException异常的影响,如果有人正在迭代列表并且它以除迭代器方法以外的任何其他方式进行修改。

编辑:

另外,你的 GroupItemSampleComparator 不好,它必须在传递的两个对象被它们的equals()方法认为相等时返回0。尝试使用以下代码(假设getStartDate()返回long):

public int compare(WmGroupItemSample s1, WmGroupItemSample s2) {
    long diff = s2.getStartDate() - s1.getStartDate();
    return diff > 0 ? -1 : diff < 0 ? 1 : 0;
}

4
也许这可以帮助解决问题——考虑到可能存在列表的其他访问方式,以下引用自synchronizedList(List<T> list) Javadoc:
返回由指定列表支持的同步(线程安全)列表。为了保证串行访问,必须确保通过返回的列表完成对后端列表的所有访问。
在迭代返回的列表时,用户必须手动对该列表进行同步,这是非常重要的:
List list = Collections.synchronizedList(new ArrayList());
  ...
synchronized (list) {
    Iterator i = list.iterator(); // Must be in synchronized block
    while (i.hasNext())
        foo(i.next());
}

所以,所有对这个列表的迭代都是这样保护的吗?

3

这个异常不仅会在多线程环境下出现,例如,如果你在迭代一个列表并且在迭代时移除了一个元素,那么这个异常也可能发生(取决于你移除元素的方式)。


我知道,但为什么异常指向Collections.sort?我可以理解排序过程会删除和添加项目以达到排序目的。我该如何摆脱这个问题? - snaggs
@fessy,你能提供一下你的GroupItemSampleComparator吗? - sp00m
添加了GroupItemSampleComparator类。 - snaggs

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