目录
- 简介
- JDK1.7
- JDK1.8
- 重要属性
- Node类
- ForwardingNode类
- 原子操作和Unsafe类
- 重要方法
- 初始化表操作(initTable)
- 插入键值对(put和putVal)
- helpTransfer
- 扩容操作(transfer)
- addCount()
- 总结
- Reference
由于HashMap是非线程安全的,而且HashTable和Collections.synchronizedMap()的效率很低(基本上是对读写操作加锁,一个线程在使用,其他线程必须等待)。因此可以使用并发安全的ConcurrentHashMap。
ConcurrentHashMap的实现原理和HashMap有很多相似之处,所以看了HashMap的源码后对于理解ConcurrentHashMap有很大的好处。
ConcurrentHashMap采用 分段锁(Segments) 的机制,底层采用数组+链表的存储结构。
Segments继承了 ReentrantLock 用来充当锁的角色,每个Segment保护哈希表(table[])的若干个桶(HashBucket)。
JDK1.8已经不使用分段锁机制来保证并发安全了,而是使用 CAS+Synchronized 来保证,底层使用数组+链表+红黑树的存储结构(类似于HashMap的改变)。
以下是一些会用到的属性,部分在HashMap已经出现过。
其中比较重要的是 sizeCtl,这个标志控制了很多状态。
Node类
这里的Node类不允许直接setValue(),并且val和next使用了volatile修饰保证了可见性。
ForwardingNode类
这个类对于ConcurrentHashMap很重要,是实现并发的核心之一。

这个类是用来标识table[]上的Node的,当表上的结点是 ForwardingNode 类时,说明这个结点已经被复制了,不需要再对这个结点进行操作了。
在后面很多方法中都有这个类的出现。
这里有三个重要的原子操作,使用这些操作而不需要加锁保证了ConcurrentHashMap的性能。
Unsafe类提供了很多操作。例如获取元素的地址等和各种CAS操作。
可以看下 Unsafe介绍
初始化表操作(initTable)
这个方法的目的是初始化一个table。
插入键值对(put和putVal)
put操作和putVal的操作的关系只是一个调用关系,在这就不提put操作了,重点在于putVal。
这里的操作流程是:
- 先计算传入key的哈希值hash。
- 进入for循环自旋直到完成插入操作。
- 如果表还未初始化,则去初始化表。
- 如果hash位置对应的桶还未初始化,就用CAS操作去插入新的键值对并退出自旋。
- 如果是ForwardingNode就调用 helpTransfer() 去帮忙将旧表复制到新表中。
- 否则就是需要将新的键值对放到链表或者树上了。(具体看代码)
- 最后调用 addCount() 使得元素数目+1,这里如果不够空间也会在 addCount() 中扩容。
helpTransfer
这个方法是发现结点是 ForwardingNode 类时候调用的,进入其中帮助 transfer。
扩容操作(transfer)
当空间不够的时候,要将旧的表(table)复制到新的表(nextTab)中。这是个人觉得最复杂的操作了。
操作流程:
- 如果新的表为空,就初始化新的表(单线程操作),新的表的容量为原来的2倍。
- 新建一个 ForwardingNode 用来标志已经完成扩容的结点。
- 自旋直到处理完毕
- 为线程分配工作的区间,逆序处理每一个桶。
- 遍历桶。
- 如果该桶为空,就将 ForwardingNode 放入标志已经处理过。
- 如果该桶为 ForwardingNode 就跳过。
- 否则使用 synchronized 锁住桶,进行复制转移操作(详见注释,和HashMap有点像)。完成后,标记为 ForwardingNode。
- 当finish后,则将原来的table更新为nextTab,然后将nextTable设为null帮助GC。
addCount()
addCount,更新baseCount并判断table数组是否太小需要扩容。
核心思想是在并发较低时,只需更新base值。在高并发的情况下,将对单一值的更新转化为数组上元素的更新,以降低并发争用。总的映射个数为base+CounterCell各个元素的和。如果总数大于阈值,扩容。
- JDK1.8使用了CAS操作和synchronized来实现并发,但是这里的synchronized只是锁住正在操作的桶而已,并没有锁住整个map。而JDK1.7使用了分段锁来实现并发,一个segment对应了多个桶。
- JDK1.8使用链表+红黑树来实现,在冲突很多的情况下时间复杂度优化了许多。
- 阅读了HashMap源码后再看ConcurrentHashMap简单了一些,两者有很多共通之处。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/194210.html