哈希映射和可见性

2022-09-04 02:51:10

HashMap的javadoc指出:

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

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

  • 它确实会像Java 7中预期的那样立即失败
  • 但它(似乎)总是与Java 6一起使用(即它不会引发承诺的异常)。

注意:它有时在Java 7中不会失败(比如20个时间中的1个) - 我想它与线程调度有关(即2个runnable不是交错的)。

我错过了什么吗?为什么使用Java 6运行的版本没有抛出一个ConceptModificationException?

实质上,有 2 个可运行的任务并行运行(倒计时用于使它们大约同时启动):

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

然后,主线程检查已添加到数组中的键数。

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 位版本)。


答案 1

如果我们研究源代码并比较Java 6和Java 7之间的它们,我们将看到这样一个有趣的差异:HashMap

transient volatile int modCount;在Java6中,只是在Java7中。transient int modCount;

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

        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)我创建了类,它绝对是Java 6的副本。HashMap2HashMap

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

transient volatile Set<K>        keySet = null;

transient volatile Collection<V> values = null;

2)然后我在测试这个问题时用了这个,并在Java 7下运行它HashMap2

结果:它的工作方式类似于 下的此类测试,即没有任何 .Java 6ConcurentModificationException

这一切都证明了我的猜想。断续器


答案 2

作为旁注,(尽管名称不幸)不打算检测跨多个线程的修改。它仅用于捕获单个线程中的修改。无论使用迭代器还是其他任何方式,跨多个线程修改共享哈希映射(没有正确同步)的效果都保证会被破坏。ConcurrentModificationException

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