如何certificatejava中的HashMap不是线程安全的

我正在开发应用程序,它将HashMap作为共享状态。 我需要通过unit testingcertificate它在multithreading环境中会有问题。

我试图通过检查这两个HashMap的大小和元素来检查sinlge线程环境和multithreading环境中的应用程序状态。 但似乎这没有帮助,状态总是一样的。

有没有其他方法来certificate它或certificate在地图上执行操作的应用程序适用于并发请求?

很难模拟Race但是查看HashMap的put()方法的OpenJDK源代码:

 public V put(K key, V value) { if (key == null) return putForNullKey(value); //Operation 1 int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } //Operation 2 modCount++; //Operation 3 addEntry(hash, key, value, i); return null; } 

如您所见, put()涉及3个不同步的操作 。 复合操作是非线程安全的 。 因此从理论上certificateHashMap不是线程安全的。

它是一个老线程。 但只是粘贴我的示例代码,它能够演示hashmap的问题。

看看下面的代码,我们尝试使用10个线程(每个线程3000个项目)将30000个项目插入到hashmap中。

因此,在完成所有线程之后,理想情况下应该看到hashmap的大小应该是30000.但是重建树时实际输出可能是exception,或者最终计数小于30000

 class TempValue { int value = 3; @Override public int hashCode() { return 1; // All objects of this class will have same hashcode. } } public class TestClass { public static void main(String args[]) { Map myMap = new HashMap<>(); List listOfThreads = new ArrayList<>(); // Create 10 Threads for (int i = 0; i < 10; i++) { Thread thread = new Thread(() -> { // Let Each thread insert 3000 Items for (int j = 0; j < 3000; j++) { TempValue key = new TempValue(); myMap.put(key, key); } }); thread.start(); listOfThreads.add(thread); } for (Thread thread : listOfThreads) { thread.join(); } System.out.println("Count should be 30000, actual is : " + myMap.size()); } } 

输出1:

 Count should be 30000, actual is : 29486 

输出2 :(例外)

 java.util.HashMap$Node cannot be cast to java.util.HashMap$TreeNodejava.lang.ClassCastException: java.util.HashMap$Node cannot be cast to java.util.HashMap$TreeNode at java.util.HashMap$TreeNode.moveRootToFront(HashMap.java:1819) at java.util.HashMap$TreeNode.treeify(HashMap.java:1936) at java.util.HashMap.treeifyBin(HashMap.java:771) at java.util.HashMap.putVal(HashMap.java:643) at java.util.HashMap.put(HashMap.java:611) at TestClass.lambda$0(TestClass.java:340) at java.lang.Thread.run(Thread.java:745) 

但是,如果修改Map myMap = new HashMap<>(); 到ConcurrentHashMap ,输出总是30000。

另一个观察 :在上面的例子中, TempValue类的所有对象的哈希码是相同的(**即1 **)。 所以你可能想知道,只有在发生冲突(由于hashcode)的情况下才会出现HashMap的这个问题。 我尝试了另一个例子。

将TempValue类修改为

 class TempValue { int value = 3; } 

现在重新执行相同的代码。
在每5次运行中,我看到2-3次运行仍然提供不同于30000的输出
因此,即使您通常没有太多碰撞,您仍可能会遇到问题。 (可能是由于重建HashMap等)

总的来说,这些示例显示了ConcurrentHashMap处理的HashMap问题。

我需要通过unit testingcertificate它在multithreading环境中会有问题。

这将是非常困难的。 种族条件很难certificate。 您当然可以编写一个程序,它可以在大量线程中放入并进入HashMap,但是记录,`volatile字段,其他锁定以及应用程序的其他时序细节可能会使您的特定代码失败变得非常困难。


这是一个愚蠢的小HashMap失败测试用例。 它失败是因为它因为HashMap损坏而进入无限循环而超时。

 @Test(timeout = 10000) public void runTest() throws Exception { final Map map = new HashMap(); ExecutorService pool = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { pool.submit(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { map.put(i, "wow"); } } }); } pool.shutdown(); pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); } 

这很容易certificate。

在意识形态

哈希映射基于数组,其中每个项表示一个存储桶。 随着更多密钥的增加, 存储桶会增长,并且在某个阈值时, arrays会以更大的尺寸重新创建 ,其存储桶会重新排列,以便更均匀地传播(性能考虑因素)。

技术上

这意味着有时HashMap#put()将在内部调用HashMap#resize()以使底层数组更大。

HashMap#resize()table字段分配一个具有更大容量的新空数组 ,并用旧项填充它。 当这种情况发生时,底层数组不包含所有旧项,并且使用现有键调用HashMap#get()可能返回null

以下代码演示了这一点。 您很可能会得到exception,这意味着HashMap不是线程安全的。 我选择目标键为65 535 – 这样它将是数组中的最后一个元素,因此是重新填充期间的最后一个元素,这增加了在HashMap#get()上获取null的可能性HashMap#get() (看看为什么,请参阅HashMap#put()实现)。

 final Map map = new HashMap<>(); final Integer targetKey = 0b1111_1111_1111_1111; // 65 535 final String targetValue = "v"; map.put(targetKey, targetValue); new Thread(() -> { IntStream.range(0, targetKey).forEach(key -> map.put(key, "someValue")); }).start(); while (true) { if (!targetValue.equals(map.get(targetKey))) { throw new RuntimeException("HashMap is not thread safe."); } } 

一个线程将新键添加到地图中。 另一个线程不断检查targetKey是否存在。

如果计算这些例外情况,我会得到大约200 000

正在阅读API文档吗? 那里有一个声明:

请注意,此实现不同步。 如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步。 (结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键相关联的值不是结构修改。)这通常通过同步自然封装映射的某个对象来完成。 。 如果不存在此类对象,则应使用Collections.synchronizedMap方法“包装”该映射。 这最好在创建时完成,以防止意外地不同步访问地图:

线程安全的问题在于通过测试很难certificate。 大部分时间都可以。 你最好的选择就是运行一堆正在获取/放置的线程,你可能会遇到一些并发错误。

我建议使用ConcurrentHashMap并相信Java团队说HashMap不同步就足够了。

有没有其他方法可以certificate这一点?

如何阅读文档 (并注意强调的“必须”):

如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步

如果您打算尝试编写一个演示错误行为的unit testing,我建议如下:

  • 创建一组具有相同哈希码的键(例如30或40)
  • 为每个键添加值到地图
  • 为密钥生成一个单独的线程,它有一个无限循环,(1)断言键存在于映射中,(2)删除该键的映射,(3)添加映射。

如果幸运的话,断言会在某些时候失败,因为哈希桶后面的链表会被破坏。 如果你运气不好,尽管有文档certificateHashMap确实是线程安全的。

这可能是,但永远不会是一个完美的测试。 竞争条件太难以预测了。 话虽这么说,我写了一个类似的测试,以帮助修复专有数据结构的线程问题,在我的情况下,更容易certificate出错(在修复之前),而不是certificate什么都不会发生错(修复后)。 您可能构建一个multithreading测试,最终会因足够的时间和正确的参数而失败。

这篇文章可能有助于确定您的测试中要关注的领域,并对可选替换有一些其他建议。

您可以创建多个线程,每个线程都将一个元素添加到一个hashmap并迭代它。 即在r​​un方法中,我们必须使用“put”然后使用迭代器进行迭代。

对于HashMap的情况,我们得到ConcurrentModificationException,而对于ConcurrentHashMap我们得不到。