为什么HashMap在多个线程中没有保证?

时间:2014-07-23 01:50:26

标签: java multithreading hashmap

我有seen something解释了HashMap在多线程中无法保持原因的原因。

它说,当调整大小时,链表中的整个对象序列都会反转,并显示示例:

  

例如,假设有3个具有相同哈希码的密钥,因此存储在桶内的链表中[以下格式在object_value(current_address,next_address)中]      初始结构:1(100,200) - > 2(200,300) - > 3(300,null)
     在通过线程1:3(300,200)调整大小后 - > 2(200,100) - > 1(100,null)
     当thread-2开始调整大小时,它再次从第一个元素开始,将它放在头部:
  1(100,300) - > 3(300,200) - > 2(200,100)==>这成为下一次插入的无限循环,线程在这里挂起。

我对这个例子感到很困惑,

初始结构:1 - > 2 - > 3

线程1:3-> 2-> 1

线程2:1 - > 3 - > 2为什么?

有人可以帮我分析一下这个例子或者展示一个更详细的例子吗?感谢。

3 个答案:

答案 0 :(得分:1)

我不清楚你在问什么。

您是否有兴趣知道为什么HashMap不是线程安全的?或者您只是想知道调整大小期间“逆转”效果的原因(这是线程不安全的原因之一)?

对于后一个问题(这是你在问题中明确提出的问题),原因如下:

通过检查HashMap的源代码,有一个transfer()方法负责将条目从旧表移动到新表:

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

相反的是上述逻辑的副作用(看起来更接近do-while循环并且它应该不难理解)。

如果您问他们为什么要让订单反转,那么您最好问作者。但是我可以告诉你,他们并不打算“逆转”这个命令。由于HashMap没有关于迭代顺序的保证,实现不需要维护任何顺序。只要结果正确,实现者就可以选择最简单,最快捷的方式来实现调整大小逻辑。目前的逻辑是他们的选择。


更新:如果你只是想知道一个非线程安全的案例,还有其他更明显的事情。

例如,在向Map添加条目时,逻辑如下:首先计算要放入条目的索引,将其添加到表中的索引,如果表是“full”,则执行resize。

可能存在这样的情况:第一个线程试图添加一个条目,原始表大小为100,然后哈希码为101,然后它找出索引为1.

此时,另一个线程进入,并向表中添加一个条目,发现该表是“full”,然后执行resize。新表的大小现在是200。

然后在这个时候,线程1进入实际将条目放入表并尝试放入索引1的步骤。但是,对于大小为200的新表,要放置的正确索引应该是101而不是1.

结果是地图导致了损坏的状态。

还有更多不同的线程无法保证的例子。


对于您提到的给定示例。以下是它可能导致问题的具体示例:

假设现有哈希表:

[0] -> E1 -> E2 -> E3 -> null
[1]

Resize将执行以下操作:

- Create a new table
(old table)
[0] -> E1 -> E2 -> E3 -> null
[1]

(new table)
[0]
[1]
[2]
[3]


- iterate thru the original entries, and put it one by one 

(Put E1 to new table)
[0] -       E2 -> E3
[1]  \
      \
       v
[0]  -> E1 ->null
[1]
[2]
[3]

(Put E2 to new table)
[0] ------      E3
[1]        \
            \
             v
[0]  -> E2 -> E1 ->null
[1]
[2]
[3]

此时您将看到旧表的索引0仍然指向E1

如果另一个线程进入并尝试进行调整大小,在这种中间状态下调整大小可能会导致各种问题:原始文章中的next错误,或者在结果表中缺少条目等。< / p>

答案 1 :(得分:0)

来自Oracle Java文档(http://docs.oracle.com/javase/7/docs/api/java/util/HashMap.html

  

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

Map m = Collections.synchronizedMap(new HashMap(...));

当您向hashmap添加元素时,它的内部结构会发生变化,因此您无法信任hashmap中元素的排序。如果要维护排序,请使用TreeMap。

答案 2 :(得分:0)

我从这里得到答案:

http://mailinator.blogspot.hu/2009/06/beautiful-race-condition.html

另外,谢谢