【问题标题】:Why hashmap is not thread safe? [duplicate]为什么hashmap不是线程安全的? [重复]
【发布时间】:2018-09-27 05:47:16
【问题描述】:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;


public class TestLock {
    private static ExecutorService executor = Executors.newCachedThreadPool();
    private static Map<Integer, Integer> map = new HashMap<>(1000000);
    private static CountDownLatch doneSignal = new CountDownLatch(1000);

    public static void main(String[] args) throws Exception {

        for (int i = 0; i < 1000; i++) {
            final int j = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    map.put(j, j);
                    doneSignal.countDown();
                }
            });
        }
        doneSignal.await();
        System.out.println("done,size:" + map.size());
    }
}

有人说hashmap插入在并发的时候是不安全的。因为hashmap会进行扩容操作,但是我这里设置大小为1000000,只会扩容到750000。我在这里做了 1000 次插入,所以我不会扩展它。所以应该没有问题。但是结果总是小于1000,怎么回事?

【问题讨论】:

  • 线程安全问题不仅限于展开操作。任何修改操作都可能导致竞争条件、可见性问题、原子性问题。
  • 为了说明,这个在每次放置时执行的简单操作不是线程安全的:++size
  • 不,不是“某些人”说HashMap 不是线程安全的,而是the documentation。提示:如果一个类没有在其文档中明确承诺线程安全,请始终假定它是不是线程安全的。

标签: java


【解决方案1】:

为什么hashmap不是线程安全的?

因为 javadocs 是这么说的。见下文。

你说:

有人说hashmap插入并发时不安全。

不仅仅是“某些人”1javadocs 清楚地说明了这一点:

"注意这个实现是不同步的。如果多个线程同时访问一个hash map,并且至少有一个线程在结构上修改了map,那么它必须在外部同步。(一种结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键关联的值不是结构修改。"

你问:

我在这里做了 1000 次插入,所以我不会扩展它。所以应该没有问题。但是结果总是小于1000,怎么回事?

您需要考虑的不仅仅是散列数组的扩展。这不仅仅是插入。 任何对HashMap 进行结构修改的操作都需要同步...否则您可能会得到未指定的行为2

这就是你得到的。


1 - 我强烈建议您不要依赖直觉或“某些人”所说的话。相反,花时间阅读并理解相关规范;即 javadocs 和 Java 语言规范。
2 - 在此示例中,您可以通过阅读 HashMap 源代码轻松了解为什么会出现未指定行为。例如在 OpenJDK Java 11 源代码中,size() 不是 synchronized,它返回 private transient int size 字段的值。这不是线程安全的。当其他线程添加或删除映射条目时,它们将更新size,调用size() 的线程可能会得到一个陈旧的值。

【讨论】:

  • 非常感谢,我会研究更多的文档。
【解决方案2】:

“因为hashmap会进行扩容操作”不仅是HashMap不是线程安全的原因。

您必须参考 Java 内存模型才能了解它可以提供什么保证。

这样的保证之一是可见性。这意味着除非满足特定条件,否则在一个线程中所做的更改可能在其他线程中不可见。

【讨论】:

    【解决方案3】:

    好吧,问题标题并没有真正描述您的要求。总之,

    在这里您将容量设置为 1000000。不是大小。

    Capacity :最初在这个 hashmap 中有多少个槽。基本上 空槽。

    Size : 地图中填充的元素数量。

    因此,即使您将容量设置为 1000000,最后您也没有那么多元素。所以map中填充的元素个数会通过.size()方法返回。它与并发问题无关。是的,由于多种原因,HashMap 不是线程安全的。

    【讨论】:

    • 我做了 1000 次插入。
    • 这很明显,因为您尝试在插入过程中通过主线程查看地图中的元素数量。但是,您可以获得的最佳情况是 1000。肯定不是 1000000。取决于执行程序线程在主线程读取时在该特定实例中添加了多少元素,您的主线程读取将被更改。
    【解决方案4】:

    如果您在 HashMap 类 here 中看到“put”的实现,则没有使用“同步”的地方,尽管它会执行许多线程不安全的操作,例如如果未找到 key 的哈希创建 TreeNode,增加 modCount 等。

    ConcurrentHashMap 适合您的用例

    【讨论】:

      【解决方案5】:

      如果您需要线程安全的HashMap,您可以改用Hashtable 类。

      与新的集合实现不同,Hashtable 是同步的。如果不需要线程安全的实现,建议使用 HashMap 代替 Hashtable,关于 Hashtable 的 javadoc 说。

      同样,如果有一天你需要一个线程安全的ArrayList,请使用Vector

      编辑:哦,我建议了错误的方法。我的道歉! cmets 提出了比我更好的解决方案: Collections.synchronizedXxx()ConcurrentHashMap,这对这个问题的开启者有用。

      【讨论】:

      • 请不要。不应再使用这些类。如果您想要同步集合,请使用Collections.synchronizedXxx()
      • 或者 - 如 Hashtable 的文档所述 - ConcurrentHashMap
      • 更改为 ConcurrentHashMap 返回正确的大小为 1000。
      • @mtj 实际上,Java 10 中的HashMap documentation 没有提到ConcurrentHashMap。它只讨论:Map m = Collections.synchronizedMap(new HashMap(...));
      • @Marc 欢迎您随时编辑您的回答,以纳入这些评论中建议的改进。
      猜你喜欢
      • 1970-01-01
      • 2012-03-22
      • 2012-11-20
      • 2013-10-11
      • 2020-10-10
      • 1970-01-01
      • 2011-10-14
      • 2016-08-14
      • 2023-03-12
      相关资源
      最近更新 更多