假设有一个ConcurrentHashMap
并且有两个线程。
如果两个线程都从同一个桶中读取一些数据,那么我的理解是两者都可以同时读取该桶,因为CHM不会阻止读取操作。
但假设一个线程正在写一个(put
)到一个桶。然后,第二个线程可以同时从同一个桶读取(get
)还是第二个线程必须等待put
操作完成?
如果是Hashtable
,则get
必须等到put
操作完成。但在CHM的情况下它会如何表现?
答案 0 :(得分:5)
没有必要进行猜测。 source code for ConcurrentHashMap已打开,任何人都可以阅读。 (这是JDK 8 build 128,第一个JDK 8发布候选版本。)
你应该没有理解它,因为它只有6,300行。 :-)实际上,其中很大一部分是注释,大部分代码都用于处理边缘情况。 get()和put()的简单路径并不是非常复杂,只有几十行代码。
您对读取操作(get(),contains())的理解是正确的;没有阻挡。如果需要,哈希到桶并在桶内搜索是直截了当的,没有锁定。易失性读取确保了内存可见性。 (在第622-623行,val
的{{1}}和next
字段是易失性的。)读取操作与其他读取同时进行,也可以写入同一个存储桶。
删除和替换值的策略非常简单,因为在搜索和修改存储桶时,存储桶的头部被锁定。请参阅Node
第1117行的synchronized
块。添加到现有存储桶的replaceNode
类似;请参阅put
第1027行的synchronized
块。这些操作当然会阻止其他线程尝试删除,替换或添加相同存储桶的条目。如果值正在被替换,则获取此键值的线程将看到旧值或新值,具体取决于读取线程是否在值被替换之前或之后找到节点写线程。
将第一个元素放入存储桶有一种特殊情况。在第1018-1020行,如果putVal
发现存储桶为空,它将创建一个新节点并将CAS(比较并交换)到位。如果成功,则操作完成。如果两个线程试图将节点同时或多或少地添加到同一个桶中,则第一个的CAS将成功,而第二个的CAS将失败。但请注意,此代码位于for循环内(第1014行)。 CAS失败的线程只是循环并重试。实际上,所有其他写操作都在循环内。一般方法是操作乐观地进行,但检查并发编写器。如果乐观尝试失败,则重试该操作并根据当前更新的状态通过(可能)不同的路径。
答案 1 :(得分:1)
嗨,据我所知,ConcurrentHashMap允许多个读者同时读取而不会阻塞。这是通过基于并发级别将Map划分为不同的部分并在更新期间仅锁定Map的一部分来实现的。默认并发级别为16,因此Map分为16个部分,每个部分由不同的锁控制。这意味着,16个线程可以同时在Map上运行,直到它们在Map的不同部分上运行。这使得ConcurrentHashMap在保持线程安全完整的情况下仍然具有高性能。虽然,它带有警告。由于put(),remove(),putAll()或clear()等更新操作未同步,因此并发检索可能无法反映Map上的最新更改。
我希望这会有所帮助..
答案 2 :(得分:1)
这是来自ConcurrentHashMap类的JavaDocs:
“检索操作(包括get)通常不会阻塞,因此可能与更新操作重叠(包括put和remove)。检索反映了最近完成的更新操作的结果”
答案 3 :(得分:0)
在Hashtable并发操作中将锁定整个集合,但在ConcurrentHashMap中只会锁定一个存储桶。
答案 4 :(得分:0)
来自doc:
支持检索的完全并发和可调整的哈希表 预期的更新并发性。本课程遵循相同的功能 规范为Hashtable,包括方法版本 对应于Hashtable的每种方法。但是,尽管如此 操作是线程安全的,检索操作不需要 锁定,并没有任何支持锁定整个表 一种阻止所有访问的方法。 此类可与之完全互操作 Hashtable在程序中依赖于其线程安全但不依赖于它 同步细节。
检索操作(包括get)一般不会阻塞,所以可能 与更新操作重叠(包括put和remove)。的反演 反映最近完成的更新操作的结果 坚持开始。对于诸如putAll和。之类的集合操作 清除,并发检索可能仅反映插入或删除 一些条目。同样,Iterators和Enumerations返回元素 反映哈希表的状态在某个时刻或之后的某个时刻 创建迭代器/枚举。他们不扔 ConcurrentModificationException的。但是,迭代器的设计是 一次仅由一个线程使用。
因此,您不应期望操作与Hashtable完全同步,但相同(一系列)操作是线程安全的。第二个突出显示的句子并不意味着,但在我看来强烈建议,这里发生了什么:put
正在进行中,即未完成,不会阻止get
- get
我们根本看不到变化。
虽然我没有完成整个CHM课程,但这篇文档支持我的假设(取自OpenJDK 6)
static final class Segment<K,V> extends ReentrantLock implements Serializable {
/*
* Segments maintain a table of entry lists that are always
* kept in a consistent state, so can be read (via volatile
* reads of segments and tables) without locking. This
* requires replicating nodes when necessary during table
* resizing, so the old lists can be traversed by readers
* still using old version of table.
当更新“完成”时似乎没有明确定义;通常,一旦将新桶链接到桶列表中,我猜。 CHM还大量使用volatile字段来确保线程读取列表中最新的桶。