Java 1.8.0_45版本中才会出现ConcurrentModificationException异常。

4
我对这段代码有两个问题:
import java.util.*;

public class TestClass {

    private static List<String> list;   
    public static void main(String[] argv) {

        list = generateStringList(new Random(), "qwertyuioasdfghjklzxcvbnmPOIUYTREWQLKJHGFDSAMNBVCXZ1232456789", 50, 1000);

//      Collections.sort(list, new Comparator<String>() {
//          public int compare(String f1, String f2) {
//              return -f1.compareTo(f2);
//          }
//      });

        for (int i = 0; i < 500; i++) {
            new MyThread(i).start();
         }

    }

    private static class MyThread extends Thread  {
        int id;
        MyThread(int id) { this.id = id; }
        public void run() {

            Collections.sort(list, new Comparator<String>() {
                public int compare(String f1, String f2) {
                    return -f1.compareTo(f2);
                }
            });

            for (Iterator it = list.iterator(); it.hasNext();) {
                String s = (String) it.next();
                try {
                    Thread.sleep(10 + (int)(Math.random()*100));
                }catch (Exception e) { e.printStackTrace(); }

                System.out.println(id+" -> "+s);
            }           
        }       
    }

    public static List<String> generateStringList(Random rng, String characters, int length, int size)
    {
        List<String> list = new ArrayList<String>();
        for (int j = 0; j < size; j++) {
            char[] text = new char[length];
            for (int i = 0; i < length; i++)
            {
                text[i] = characters.charAt(rng.nextInt(characters.length()));
            }
            list.add(new String(text));
        }
        return list;
    }
}

在Java 1.8.0_45上运行此代码,我得到了“java.util.ConcurrentModificationException”异常。
1)如果我取消注释线程.start之前的排序,为什么仍然会出现异常?
2)为什么只有在Java 1.8.0_45上才会出现异常?在1.6.0_45、1.7.0_79和1.8.0_5上都可以正常工作。
4个回答

10

@nbokmans 已经给出了你出现异常的一般原因。然而,这似乎与版本有关。我将解释为什么在Java 8.0_45中会出现该问题,但在1.6.0_45、1.7.0_79和1.8.0_5中不会出现。

这是由于Collections.sort()在Java 8.0_20中进行了更改。这里有一篇深入的文章(链接)。根据这篇文章,在新版中,sort操作的方式如下:

public void sort(Comparator<? super E> c) {
  final int expectedModCount = modCount;
  Arrays.sort((E[]) elementData, 0, size, c);
  if (modCount != expectedModCount) {
    throw new ConcurrentModificationException();
  }
  modCount++;
}

正如文章所解释的:

与旧的 Collections.sort 不同,这个实现在列表已被排序后修改了集合的 modCount(上面第7行),即使结构本身并没有真正改变(仍然是相同数量的元素)。

因此,即使集合已经排序,它也会进行内部更改,而在更改之前则没有这样做。这就是为什么你现在会遇到异常的原因。

实际的修复方法是不要同时使用多个线程对集合进行排序。你不应该这样做。


感谢您的解释,有什么建议的修复方法吗? - ronnyfm
1
@ronnyfm,修复方法不是同时使用多个线程对集合进行排序。你不应该这样做。 - eis

3

ConcurrentModificationException是在检测到对象的并发修改(即在单独的线程中)不允许时,由相应方法抛出的异常。

你之所以会收到此异常,是因为你正在单独的线程中修改(排序)集合并遍历它。

我引用了ConcurrentModificationException javadoc中的一段话:

例如,在另一个线程在迭代集合时,通常不允许一个线程修改该集合。一般来说,在这些情况下,迭代的结果是未定义的。

在你的代码中,你启动了500个线程,每个线程都对列表进行排序和遍历。

尝试在启动线程之前对列表进行排序,并从MyThread的#run()中移除对Collections#sort的调用。


我知道为什么会出现这个异常。但我想问的是为什么它与Java版本相关,如果我在启动线程之前对列表进行排序,为什么仍然会出现这个异常。 - Alvins
抱歉,我不确定为什么您的代码在1.8.0_45之前可以正常工作。我尽力回答了。 - nbokmans

3

Java 8重新实现了Collections::sort方法以委托给List::sort方法。这样,如果对于给定的实现,列表可以实现更有效的排序算法,例如ArrayList可以利用其随机访问属性实现比LinkedList无随机访问更高效的排序算法。

ArrayList::sort的当前实现显式检查修改,因为实现是在类内部定义的,并且能够访问其内部属性。

在Java 8之前,Collections::sort方法必须自己实现实际的排序,并且不能委派。当然,该实现不能访问特定列表的任何内部属性。更通用的排序如下实现:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    Object[] a = list.toArray();
    Arrays.sort(a, (Comparator)c);
    ListIterator i = list.listIterator();
    for (int j=0; j<a.length; j++) {
        i.next();
        i.set(a[j]);
    }
}

实现首先提取元素的副本,并将排序委托给Arrays::sort的实现。这不会引起观察到的异常,因为排序是在非共享的元素副本上进行的。稍后,使用ListIterator按照排序后的数组逐个更新元素。
对于ArrayListArrayList及其迭代器跟踪结构修改的数量,即更改列表大小的修改。如果迭代器和列表的这些数字发生分歧,则迭代器可以知道列表在其自身迭代之外被修改。但它无法发现像Collections::sort实现那样的列表元素被更改。
然而,ArrayList的契约不允许在其契约中进行并发修改。尽管在Java 8之前排序不会失败,但应用排序可能会导致不正确的结果。自Java 8以来,这是第一次由实现发现的。

0
你之所以收到这个异常是因为你有不同的线程同时修改和迭代列表。
注释掉的排序没有引起问题。CME 是由线程内的排序和迭代引起的。由于你有多个线程进行排序和迭代,所以会出现 CME。这与 Java 版本无关。
看起来你的线程不需要修改列表,所以可以在创建线程的循环之前执行一次排序,然后从线程中删除它。

你能证明它不依赖于Java版本吗?我使用了OP的代码进行测试,确实在Java 7中似乎没有任何异常。 - eis
@eis - 尝试设置测试以持续运行。我怀疑你在那次运行中只是走了运。 - John McClean
1
@JohnMcClean 我不这么认为 - 测试似乎是一致的,并且似乎有一个实际的解释。我自己添加了一个答案。 - eis
@eis - 有趣 - 干得好!值得强调的是,之前的版本并不保证能够工作,只是添加了检查和抛出异常而已。例如,如果某人在一个线程上对集合进行了排序,在另一个线程上就没有可见性保证,在别处进行更改后重新排序可能会导致各种混乱。 - John McClean

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