HashMap和可见性

HashMap的javadoc声明:

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

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

  • 它确实像Java 7那样立即失败
  • 但它(似乎)始终使用Java 6(即它不会抛出承诺的exception)。

注意:它有时不会因Java 7而失败(比如20次中的1次) – 我猜它与线程调度有关(即2个runnables不是交错的)。

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

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

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

然后主线程检查已添加到arrays的键数。

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

java.util.ConcurrentModificationException
MAX i = 0

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

MAX i = 99

使用的代码

public class Test1 { public static void main(String[] args) throws InterruptedException { final int SIZE = 100; final Map map = new HashMap(); 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 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位版本)。

如果我们将查看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?bad_id = 6625725,已在最新的Java7中修复。

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

我的测试是:

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

一件重要的事情是我们需要在这里介绍2个新领域:

 transient volatile Set keySet = null; 

 transient volatile Collection values = null; 

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

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

这一切都certificate了我的猜想。 QED

另外, ConcurrentModificationException (尽管名称不幸) 并不是为了检测多个线程的修改。 它用于捕获单个线程内的修改。 无论使用迭代器还是其他任何东西,都可以保证在多个线程之间修改共享HashMap的效果(没有正确的同步)。

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

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

因此,当读取器线程上执行此行时,编写器线程已完成

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

在Java 6中,由于modCountvolatile ,迭代器会看到最新的modCount and size ,因此迭代的其余部分会顺利进行。

在Java 7中, modCount不是volatile,迭代器可能看到过时的modCount=3, size=3 。 在sleep(1) ,迭代器看到更新的modCount ,并立即失败。

这个理论存在一些缺陷:

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

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