14

HashMap 的 javadoc状态:

如果在迭代器创建后的任何时候对映射进行结构修改,除了通过迭代器自己的 remove 方法之外的任何方式,迭代器都会抛出 ConcurrentModificationException。

我构建了一个示例代码,根据规范,它应该几乎立即失败并抛出 ConcurrentModificationException;

  • 它确实像 Java 7 预期的那样立即失败
  • 但它(似乎)总是适用于 Java 6(即它不会抛出承诺的异常)。

注意:Java 7 有时不会失败(比如 20 次中有 1 次)——我猜这与线程调度有关(即 2 个可运行对象没有交错)。

我错过了什么吗?为什么使用 Java 6 运行的版本不会抛出 ConcurrentModificationException?

实质上,有 2 个 Runnable 任务并行运行(使用倒计时闩锁使它们大致同时启动):

  • 一种是将项目添加到地图
  • 另一个是遍历地图,读取键并将它们放入数组中

然后主线程检查有多少键已添加到数组中。

Java 7 典型输出(迭代立即失败):

java.util.ConcurrentModificationException
MAX i = 0

Java 6 典型输出(整个迭代经过,数组包含所有添加的键):

最大 i = 99

使用的代码

public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        final int SIZE = 100;
        final Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        map.put(1, 1);
        map.put(2, 2);
        map.put(3, 3);
        final int[] list = new int[SIZE];
        final CountDownLatch start = new CountDownLatch(1);
        Runnable put = new Runnable() {
            @Override
            public void run() {
                try {
                    start.await();
                    for (int i = 4; i < SIZE; i++) {
                        map.put(i, i);
                    }
                } catch (Exception ex) {
                }
            }
        };

        Runnable iterate = new Runnable() {
            @Override
            public void run() {
                try {
                    start.await();
                    int i = 0;
                    for (Map.Entry<Integer, Integer> e : map.entrySet()) {
                        list[i++] = e.getKey();
                        Thread.sleep(1);
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        };
        ExecutorService e = Executors.newFixedThreadPool(2);
        e.submit(put);
        e.submit(iterate);
        e.shutdown();

        start.countDown();
        Thread.sleep(100);
        for (int i = 0; i < SIZE; i++) {
            if (list[i] == 0) {
                System.out.println("MAX i = " + i);
                break;
            }
        }
    }
}

注意:在 x86 机器上使用 JDK 7u11 和 JDK 6u38(64 位版本)。

4

3 回答 3

9

如果我们查看HashMap源代码并在 Java 6 和 Java 7 之间进行比较,我们会看到这样有趣的差异:

transient volatile int modCount;在 Java6 和transient int modCount;Java7 中。

我确信这是由于以下原因导致提到的代码的不同行为:

        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();

UPD:在我看来,这是一个已知的 Java 6/7 错误:http : //bugs.sun.com/bugdatabase/view_bug.do?bug_id=6625725 已在最新的 Java7 中修复。

UPD-2: @Renjith 先生说,他只是测试并没有发现 HashMaps 实现的行为有任何差异。但我也刚刚测试过。

我的测试是:

1)我创建了HashMap2类,它绝对是HashMap Java 6的副本。

一件重要的事情是我们需要在这里引入 2 个新字段:

transient volatile Set<K>        keySet = null;

transient volatile Collection<V> values = null;

2)然后我在这个问题的测试中使用它并在Java 7下HashMap2运行它

结果:它像这样的测试在 下工作Java 6,即没有任何ConcurentModificationException

这都证明了我的猜想。量子点

于 2013-01-16T16:50:10.140 回答
6

作为旁注,ConcurrentModificationException(尽管名称很不幸)并非旨在检测跨多个线程的修改。它用于捕获单个线程中的修改。无论使用迭代器或其他任何东西,都保证会破坏跨多个线程(没有正确同步)修改共享 HashMap 的效果。

简而言之,无论 jvm 版本如何,您的测试都是虚假的,并且它完全没有任何不同只是“运气”。例如,由于 HashMap 内部在查看跨线程时处于不一致状态,此测试可能会引发 NPE 或其他一些“不可能的”异常。

于 2013-01-16T17:00:40.757 回答
1

我的理论是,在 Java 6 和 7 上,在读取器线程中创建迭代器比在写入器线程中放置 100 个条目需要更长的时间,主要是因为必须加载和初始化新类(即EntrySet, AbstractSet, AbstractCollection, Set, EntryIterator, HashIterator, Iterator

因此,在阅读器线程上执行此行时,编写器线程已经完成

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry

在 Java 6 中,因为modCountis volatile,迭代器看到了 latest modCount and size,所以其余的迭代顺利进行。

在 Java 7 中,modCount不是 volatile,迭代器可能会看到 stale modCount=3, size=3。之后sleep(1),迭代器看到 updated modCount,并立即失败。

这个理论的一些缺陷:

  1. MAX i=1该理论应该在 java 7 上进行预测
  2. 在 main() 执行之前,HashMap可能已经被其他代码迭代,所以提到的类可能已经加载了。
  3. 阅读器线程可能会看到陈旧modCount,但不太可能,因为这是该线程上变量的第一次读取;没有先前的缓存值。

我们可以通过在 Hashmap 中植入日志代码来找出阅读器线程看到的内容,从而解决这个问题。

于 2013-01-16T17:50:04.717 回答