概况图
元素插入流程图
扩容流程图
ForwardingNode和ReservationNode
ForwardingNode在table扩容时使用,内部记录了扩容后的table,即nextTable。当table需要进行扩容时,依次遍历当前table的每一个槽,如果不为null,则需要把其中所有的元素根据hash值放入扩容后的nextTable中,而原table的槽内会放置一个ForwardingNode节点。正如其名,此节点会把find请求转发到扩容后的nextTable上,而执行put方法的线程如果碰到此节点,也会协助进行迁移。
ReservationNode在computeIfAbsent()及其相关方法中作为一个预留节点使用。computeIfAbsent()方法会判断相应的key值是否已经存在,如果不存在,则调用由用户实现的自定义方法来生成Value值,组成KV键值对,随后插入此哈希集合汇总,在并发场景下,在从得知Key不存在到插入哈希集合的时间间隔内,为了防止哈希槽被其他线程抢占,当前线程会使用一个ReservationNode节点放到槽中并加锁,从而保证了线程的安全性。
java7与java8比较
在jdk1.7中是采用Segment + HashEntry + ReentrantLock的方式进行实现的,
而1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现。
JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档。
在 JDK8 中:ConcurrentHashMap 进行了巨大改动。它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式实现(JDK8 中HashMap的实现,当链表长度超过阈值[8]时,将链表转换为红黑树,这样大大减少了查找时间),但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
ConcurrentHashmap(1.8)这个并发集合框架是线程安全的,当你看到源码的get操作时,会发现get操作全程是没有加任何锁的,这也是它比其他并发集合比如 HashTable、用Collections.synchronizedMap()包装的HashMap 安全效率高的原因之一。
get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和 Node 数组用volatile修饰没有关系(这里的volatile是为了使得Node数组在扩容的时候对其他线程具有可见性),正是通过Node成员的volatile保证了读到的数据不是脏数据。
ConcurrentHashMap的弱一致性
ConcurrentHashMap的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。
类似的情况还会出现在ConcurrentHashMap的另一个方法:1
2//批量添加m中的键值对到当前Map
public void putAll(Map<? extends K, ? extends V> m)
该方法并非原子操作,而是调用put方法逐个元素进行添加的,在该方法没有结束的时候,部分修改效果就会体现出来。
get无锁
1 |
|
扩容过程
《Java源码分析》:ConcurrentHashMap JDK1.8
https://blog.csdn.net/u010412719/article/details/52145145
并发编程——ConcurrentHashMap扩容逐行分析
https://www.jianshu.com/p/2829fe36a8dd
允不允许null?
ConcurrentHashmap 和 Hashtable 都是不允许 null 的 key 和 value 的,而 HashMap 允许,这是为什么呢?
这样一对比,就很容易联想到是由于并发问题引起的。
Doug Lea 是这么说的:
The main reason that nulls aren’t allowed in ConcurrentMaps
(ConcurrentHashMaps, ConcurrentSkipListMaps) is that
ambiguities that may be just barely tolerable in non-concurrent
maps can’t be accommodated. The main one is that if
map.get(key) returns null, you can’t detect whether the
key explicitly maps to null vs the key isn’t mapped.
In a non-concurrent map, you can check this via map.contains(key),
but in a concurrent one, the map might have changed between calls.
大概意思是,在并发下,如果 map.get(key) = null,ConcurrentMap 无法判断 key 的 value 为null,还是 key 不存在。
但是 HashMap 只考虑在非并发下运行,可以用 map.contains(key) 来做判断。
大师还说
I personally think that allowing
nulls in Maps (also Sets) is an open invitation for programs
to contain errors that remain undetected until
they break at just the wrong time. (Whether to allow nulls even
in non-concurrent Maps/Sets is one of the few design issues surrounding
Collections that Josh Bloch and I have long disagreed about.)Collections that Josh Bloch and I have long disagreed about.)
Doug Lea 大师也说了,自己对 HashMap 允许 null 也是有争议的。这样做只能等到程序报错才发现错误。
源码
1 | * |
参考
https://www.cnblogs.com/swiftma/p/6557685.html
https://albenw.github.io/posts/df45eaf1/
https://www.cnblogs.com/zerotomax/p/8687425.html
https://www.cnblogs.com/200911/p/5800493.html 1.7的CHM源码解读