开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情
当Hashtable和HashMap已经存在的时候,它还有什么用呢?它在性能和线程安全方面真的有效吗?它如何处理重哈希?
今天,我将深入研究ConcurrentHashMap,并试图回答所有这些棘手的问题。在深入讨论之前,我想强调一下,ConcurrentHashMap的这个实现在Java 8之前是有效的。在Java 8中,他们做了一些改变并添加了新的方法,我将在另一篇文章中介绍这些内容。
我假设,在阅读本文之前,您一定了解Hashtable和HashMap。
因此,让我们考虑这样一个场景:需要频繁的读操作和较少的写操作,同时又不影响线程安全。
Hashtable是一个可行的解决方案吗?-所以如果一个人知道Hashtable是如何工作的,他会立即说NO。虽然哈希表是线程安全的,但在多线程场景下性能很差,因为所有的方法包括哈希表的get()方法都是同步的,因此对任何方法的调用都必须等待,直到其他在哈希表上工作的线程完成它的操作(get, put等)。
HashMap是一个可行的解决方案吗?- Hashmap可以通过让多线程同时读取Hashmap来解决性能问题。
但是Hashmap不是线程安全的,所以如果一个线程试图放入数据并需要rehash,而同时另一个线程试图从Hashmap读取数据,会发生什么呢?它将进入无限循环。
我认为正是因为上述原因,Java在5.0版本中引入了ConcurrentHashMap。因为这对双方都有好处,也能达到目的。
现在让我们深入了解这3种方法所使用的底层数据结构。Hashtable和HashMap都使用数组和链表作为存储数据的数据结构。
ConcurrentHashMap在它的顶部创建一个数组,该数组的每个索引表示一个HashMap。(在Java 8中,它是树形结构而不是链表结构,以进一步提高性能)
ConcurrentHashMap中的读、写、删除操作
a)如果观察上面的图表,很明显,无论是插入操作还是读取操作,都必须首先确定插入/读取操作应该发生的段的索引。
b)一旦确定了,就必须确定hashmap的内部桶/数组,以找到插入/读取的确切位置。
c)识别桶后,遍历链表,检查键值对。
i)在插入的情况下,如果key匹配则用新的值替换该值,否则在链表末尾插入带value的键。
ii)在read的情况下,无论key匹配,检索值并返回该值。如果没有匹配,则返回null。
iii)在删除的情况下,如果键匹配删除对应的链接该键。
ConcurrentHashMap优于HashMap/Hashtable的性能和线程安全
HashMap不是同步的,因此也不能提供任何线程安全。相比之下,Hashtable是同步的,提供线程安全,但以性能为代价。hasstable写操作使用映射范围锁,这意味着它锁定整个映射对象。
如果2个线程试图在哈希表上执行get()或put()操作,
线程T1调用哈希表上的get()/put()操作,并获得完整哈希表对象上的锁。
这使得哈希表效率低下,这就是Java提出ConcurrentHashMap的原因。
ConcurrentHashMap的工作方式有点不同,因为它每个段都需要锁,这意味着它有多个段级锁,而不是单一的映射级锁。
因此,在不同段上操作的两个线程可以获得这些段上的锁,而不会相互干扰,并且可以同时进行,因为它们正在处理不同的段锁。
线程T1调用concurrentHashMap。put(key, value),它在第1段上获得锁,并调用put方法。
线程T2调用concurrentHashMap。put(key, value),它在第15段上获得锁,并调用put方法,如下所示。
这就是ConcurrentHashMap如何提高性能并提供线程安全的方法。
多线程在ConcurrentHashMap的相同或不同段上进行同步读和写操作
读取操作:—两个线程T1和T2可以同时从ConcurrentHashMap的相同或不同段读取数据,而不会相互阻塞。
写/放操作:—两个线程T1和T2可以同时在不同的段上写数据,而不会阻塞另一个。
但是两个线程不能同时在同一段上写数据。一个人必须等待另一个人完成操作。
读写操作:—两个线程可以同时对不同段的数据进行读写操作,而不会互相阻塞。通常,检索操作不会阻塞,因此可能与写(放/移)操作重叠。get操作将返回最新更新的值,而write操作(包括put/remove)将返回最近更新的值。
LoadFactor和rehash
ConcurrentHashMap有loadFactor,它通过计算阈值(initialCapacity*loadFactor)来决定什么时候增加ConcurrentHashMap的容量,并相应地重新哈希映射。
基本上,重哈希是重新计算已经存储的条目(Key-Value对)的哈希码的过程,在达到负载因子阈值时将它们移动到另一个更大的映射。它不仅要在新的长度映射中分配项,而且当有太多的键冲突时也要这样做,这会增加一个bucket中的项,因此get和put操作的时间复杂度仍然是O(1)。
在ConcurrentHashMap中,每个段都单独重新哈希,所以线程1写入段索引1和线程2写入段索引4之间没有冲突。
例如:如果线程1正在将数据放入段[]数组索引3中,并且发现HashEntry[]数组由于超过了负载因子容量而需要重新哈希,那么它将只重新哈希段[]数组索引3中的HashEntry[]数组。HashEntry[]数组在其他段索引仍然是完整的,不受影响,继续并行服务put和get请求。
我相信这将有助于理解ConcurrentHashMap的实现和内部工作。