【问题标题】:Why the HashMap is unsafed in multiple threads? [closed]为什么 HashMap 在多个线程中是不安全的? [关闭]
【发布时间】:2014-07-23 01:50:25
【问题描述】:

我有seen something,它解释了 HashMap 在多线程中不安全的原因。

它说,当做resize时,链表中的整个对象序列被颠倒了,并给出了例子:

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

我对这个例子很困惑,

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

线程1:3->2->1

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

谁能帮我分析这个例子或展示一个更详细的例子?谢谢。

【问题讨论】:

  • 哦,拜托。当您可以更轻松地发布原始文本时,您是否必须创建和发布图片?为什么要在这个史诗般的规模上浪费时间和空间?考虑到答案是在引用的同一段中给出的,很难看出你为什么要发布这个问题。
  • 我对你的问题感到困惑:你有兴趣知道为什么内部链表被反转了吗?或者你想知道为什么 HashMap 不是线程安全的?
  • 我试着按照那篇文章的逻辑。我查看了 HashMap 的源代码以了解该无限循环是如何形成的,但看不到它。因此,我也不介意解释。

标签: java multithreading hashmap


【解决方案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 中添加条目时,逻辑是这样的:首先计算要放置条目的索引,将其添加到表中的该索引,如果表“已满”,则进行调整大小。

可能有这样一种情况,线程一试图添加一个条目,原来的表大小是100,然后哈希码是101,然后发现索引是1。

这时候,另一个线程进来,在表中添加一个条目,发现表是“满的”,然后它做resize。新表的大小现在是 200。

那么此时线程1进入到实际将表项放入表并尝试放入索引1的步骤。但是,对于大小为200的新表,正确放入的索引应该是101而不是1.

结果是地图处于损坏状态。

还有更多不同的线程不安全示例。


对于您提到的给定示例。这是一个关于它如何导致问题的具体示例:

假设现有哈希表:

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

调整大小将执行以下操作:

- 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,或结果表中缺少条目等。

【讨论】:

  • 谢谢,阿德里安,对不起,我没有清楚地描述我的问题,示例解释了多线程中会发生什么,我无法理解,我在问为什么 HashMap 不是线程安全。
  • 如果你只是想知道一个非线程安全的情况,那么还有更明显的事情。请检查我的更新
【解决方案2】:

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

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

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

当你向 hashmap 添加元素时,它的内部结构会发生变化,因此你不能相信 hashmap 中元素的顺序。如果要保持顺序,请使用 TreeMap。

【讨论】:

    【解决方案3】:

    我从这里得到了答案:

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

    另外,谢谢

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-24
      • 2012-11-20
      • 2013-10-11
      • 2020-10-10
      • 1970-01-01
      • 2020-09-29
      相关资源
      最近更新 更多