我已经意识到Java中的ConcurrentHashMap及其众多优点,但我不清楚为什么需要在每次函数调用时同步像同步HashMap这样的实现。
对我来说,感觉好像你有一个只有put(k, v)
和get(k)
功能的HashMap,那么只需要同步put
函数,因为即使你是调用put
后调整hashMap的大小,然后在执行调整大小时仍然可以授予安全的读访问权限。读者线程可以简单地从之前调整大小的版本中读取。调整大小完成后,编写器线程将替换引用,以便对get
的所有当前调用将指向调整大小的HashMap版本。
我错过了一些明显的东西吗?
答案 0 :(得分:3)
如果没有某种形式的同步,HashMap
无法保证线程安全,有几个原因。
HashMap
通过在节点中创建链接列表来处理冲突。如果列表的大小超过某个阈值,则会将其转换为树。这意味着在多线程环境中,您可以将2个线程写入并读取到树或链接列表,这显然会产生问题。
一种解决方案是并行重新创建整个链表或树,并在准备就绪时将其设置在哈希映射节点上(这基本上是“写时复制”策略),但是可怕的一部分在放置数据时,你仍然需要使用lock
(出于与下一点所述相同的原因)。 [1]
你提到过,对于resizes,我们可以使用“copy-on-write”策略,一旦新的哈希表准备就绪,我们将其原子地设置为表的引用。如果对象是不可变的(最终成员变量),这确实适用于新的Java Memory Model: [2] :
当构造函数完成时,对象被认为是完全初始化的。在该对象完全初始化之后只能看到对象引用的线程可以保证看到该对象的最终字段的正确初始化值。
但是,您仍需要根据旧的哈希表同步新哈希表的整个创建。那是因为你可以有两个racy线程同时调整哈希表的大小,你基本上会松开其中一个put
操作。
这是CopyOnWriteArrayList
中所做的事情,例如。
1 请注意,此锁的粒度可能比整个表小得多。在限制中,您可以为每个节点分别设置一个锁。对哈希表的不同部分使用不同的锁是实现线程安全性同时最小化线程争用的常用策略。
2 另一方面注意:除了long
和double
之外,对对象和基元类型的引用的赋值总是原子的。对于这些64位的类型,赛车线程只能看到32位写入的一个字。你需要让它们变得易变,以保证原子性。
答案 1 :(得分:1)
您所描述的是读取的快照语义形式。鉴于您的映射的底层实现确实支持这样的语义,因此不需要同步读取访问。但是,您不能认为这适用于任何地图实现。
具体来说:地图可能会选择separate chaining来实现其存储桶。在这种情况下,如果不同步读取访问权限,您可能最终会从正在更新列表的存储桶中读取数据。在最坏的情况下,当底层列表被实现为链表时,这种非同步访问甚至可能导致读取线程陷入无限循环。