我知道 Java 中的 ConcurrentHashMap 及其众多好处,但我不太清楚为什么像同步 HashMap 这样的实现需要在每次函数调用时同步。
对我来说,感觉就像你有一个 HashMap,其唯一的功能是 put(k, v)
和 get(k)
,那么只有 put
函数需要同步,因为即使您在调用 put
后调整 hashMap 的大小,您仍然可以在执行调整大小时授予安全读取访问权限。读者线程可以简单地从调整大小之前的版本中读取。调整大小完成后,编写器线程将替换引用,以便所有当前对 get
的调用都将指向调整大小后的 HashMap 版本。
我是否漏掉了一些明显的东西?
最佳答案
如果没有某种形式的同步,HashMap
不能成为线程安全的原因有几个。
哈希冲突
HashMap
通过在节点中创建链表来处理冲突。如果列表的大小超过某个阈值,它会将其转换为树。这意味着在多线程环境中,您可能有 2 个线程写入和读取树或链表,这显然会出现问题。
一个解决方案是并行地重新创建整个链表或树,并在准备就绪时将其设置在 HashMap 节点(这基本上是一种“写时复制”策略),但除了可怕的性能,您仍然需要在放置数据时使用锁
(出于与下一点解释相同的原因)。[1]
为表分配一个新的引用
您提到,对于调整大小,我们可以使用“写时复制”策略,一旦新的哈希表准备就绪,我们就会自动将其设置为表的引用。这确实适用于新的 Java Memory Model : [2] 如果对象是不可变的(最终成员变量):
An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.
但是,您仍然需要同步整个基于旧哈希表的新哈希表的创建。那是因为您可能有两个活跃的线程同时调整哈希表的大小,并且您实际上会丢失其中一个 put
操作。
例如,这是在 CopyOnWriteArrayList
中完成的。
1请注意,此锁的粒度可能比整个表的粒度小得多。在极限情况下,您可以为每个节点设置一个单独的锁。对哈希表的不同部分使用不同的锁是实现线程安全同时最小化线程争用的常见策略。
2另一个旁注:除了long
和double
之外,对对象和基本类型的引用赋值始终是原子的。对于这些 64 位的类型,一个 racing thread 只能看到一个 32 位的字。您需要使它们易变以保证原子性。
关于java - 写但不读时java中的同步 HashMap ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47110414/