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 6中HashMap
绝对副本。
一件重要的事情是我们需要在这里介绍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中,由于modCount
是volatile
,迭代器会看到最新的modCount and size
,因此迭代的其余部分会顺利进行。
在Java 7中, modCount
不是volatile,迭代器可能看到过时的modCount=3, size=3
。 在sleep(1)
,迭代器看到更新的modCount
,并立即失败。
这个理论存在一些缺陷:
- 该理论应该预测java 7上的
MAX i=1
- 在执行main()之前,
HashMap
可能被其他代码迭代,因此提到的类可能已经加载了。 - 读者线程看到陈旧的
modCount
是可能的,但不太可能,因为它是该线程上变量的第一次读取; 没有先前的缓存值。
我们可以通过在Hashmap中植入日志代码来找出读者线程看到的内容,从而解决这个问题的根源。