【问题标题】:Synchronized hashmap in java on write but not readjava中的同步hashmap写入但不读取
【发布时间】:2018-04-17 01:11:59
【问题描述】:

我知道 Java 中的 ConcurrentHashMap 及其众多好处,但我不太清楚为什么像同步 HashMap 这样的实现需要在每个函数调用上同步。

对我来说,感觉就像如果你有一个 HashMap,其唯一的函数是 put(k, v)get(k),那么只有 put 函数需要同步,因为即使你要在调用后调整 hashMap 的大小put,那么您仍然可以在执行调整大小时授予安全读取访问权限。阅读器线程可以简单地从之前调整大小的版本中读取。调整大小完成后,编写器线程将替换引用,以便当前对 get 的所有调用都指向调整大小后的 HashMap 版本。

我是否遗漏了一些明显的东西?

【问题讨论】:

  • 这会使 put() 效率极低:它必须复制整个 HashMap,然后在完成后使用该副本。这是 CopyOnWriteArrayList 使用的策略,但是如果您对列表进行多次写入,则效率低下。
  • 同步不是这样工作的。您需要同步写入和读取,否则读取可能会看到陈旧的或更糟糕的不一致值。
  • @assylias,我明白你为什么会得到一个陈旧的价值。但对我来说,不一致的值只有在 put 操作对已经存在的条目执行突变时才会发生。对吗?
  • @D.Rek,有趣的是,我确实认为这部分是原子的。谢谢
  • 实际上对象的分配是原子的。详情请查看我的回答。

标签: java multithreading concurrency hashmap


【解决方案1】:

如果没有某种形式的同步,HashMap 无法实现线程安全有几个原因。

哈希冲突

HashMap 通过在节点中创建链表来处理冲突。如果列表的大小超过某个阈值,它会将其转换为一棵树。这意味着在多线程环境中,您可以有 2 个线程对树或链表进行读写操作,这显然是有问题的。

一种解决方案是并行重新创建整个链表或树,并在准备好时将其设置在哈希映射节点(这基本上是“写入时复制”策略),但除了可怕的性能,您在放置数据时仍然需要使用lock(原因与下一点中解释的原因相同)。[1]

为表格分配新的引用

您提到对于调整大小,我们可以使用“写时复制”策略,一旦新的哈希表准备好,我们就将其原子地设置为表的引用。如果对象是不可变的(最终成员变量),这确实适用于新的Java Memory Model[2]

当一个对象的构造函数完成时,它被认为是完全初始化的。只有在对象完全初始化后才能看到对该对象的引用的线程,可以保证看到该对象的最终字段的正确初始化值。

但是,您仍然需要在旧哈希表的基础上同步整个新哈希表的创建。那是因为你可以有两个活泼的线程同时调整哈希表的大小,你基本上会失去put 操作之一。

这就是在CopyOnWriteArrayList 中所做的,例如。


1请注意,此锁的粒度可能比整个表小得多。在极限情况下,您可以为每个节点设置一个单独的锁。对哈希表的不同部分使用不同的锁是实现线程安全同时最小化线程争用的常用策略。

2另一个注意事项:对对象和原始类型的引用的分配始终是原子的,longdouble 除外。对于这些 64 位的类型,一个赛车线程只能看到一个 32 位的字。您需要使它们 volatile 以保证原子性。

【讨论】:

    【解决方案2】:

    您所描述的是一种用于读取的快照语义形式。鉴于您的地图的底层实现确实支持此类语义,因此无需同步读取访问。但是,您不能假设这适用于任何地图实现。

    具体来说:地图可能会选择通过separate chaining 来实现其存储桶。在这种情况下,您最终可能会在未同步读取访问时从列表正在更新的存储桶中读取。在最坏的情况下,当底层列表被实现为链表时,这种不同步的访问甚至会导致读取线程陷入无限循环。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多