前言
ConcurrentHashMap作为一个集合类,其实就是一种数据结构。类似这种源码学习我总结,分为三大块。第一就是整体思路,对于目标源码的理解。是什么,为什么,干什么,怎么用 组合拳打一通。第二就是精髓地方(包括对比同类型数据结构),也就是哪些地方设计得好,为什么好,什么时候可以借鉴、借鉴什么。另:面试的时候要说出这些方法名称显得专业,这个要背一下应该也不难。第三不同于其他jar包,作为一个数据存储结构,也要从重要的基础方法上研究,比如存数据、取数据、删数据、扩容等。
1、整体思路
是什么:集合框架中的一种并发哈希表,它允许多个线程同时访问数据而不需要进行外部同步。
底层和hashmap的数据结构设计类似,也是由数组加链表以及红黑树构成。
锁的粒度:hashtable所有方法都对整个map加锁。ConcurrentHashMap 1.7是对segment加锁,1.8是对桶加锁。
2、重要方法列举
putVal()方法,put方法实际调用putVal()。putVal()方法可以通过传入参数决定是否覆盖原来的值。初始化数组是在这个阶段完成,并不是new的时候完成的。
transfer()方法,并发扩容。扩容实际是在分发任务,所有参与扩容的线程,在做一个循环领取任务的动作(任务的拆分规则基于当前数据量,以及cpu的线程数)。
3、精妙设计点
3.1 长度定义为2的n次方好处有:
3.1.1基于这个设计,长度减一操作就可以得到二进制位都是1的数,这样使用这个数对数据key的hash值进行&运算的时候,就能够根据hash值不同位的数据的不同把他均匀地放到各个桶(数组的位置称为桶)。
3.1.2基于这个设计,扩容的时候,(因为是对数据key的hash值进行&运算决定存在哪个桶)新数据存放的索引位置要么是原数据存放的索引位置,要么是索引位置加上2的n-1次方。这样对于数据迁移产生便利性。
3.1.3基于这个设计,在数据量巨大的情况,也可以使得扩容次数变得有限(logn)。且可以直接定义较大的初始值,扩容次数将非常少。
3.2 扩容时候的lastRun机制
3.2.1 lastRun机制,因为扩容位置只可能在原索引以及原索引位置加上2的n-1次方,这两种情况。扩容的时候,一次遍历先找到最早的后续节点一致的位置,第二次遍历到这个位置的时候,直接把整个链表摘过去。减少了后续构建新节点的过程。
3.3 取数据putVal()时,如果当前状态处于扩容状态。取数据的线程会参与扩容(helpTransfer()),而不是等待扩容完毕。
3.4 管理数组位置的头结点的hash值,来代表状态,-1代表正在扩容。。这样可以让putVal的时候,仅在此处任务被分发出去扩容的时候,才会等待(其实是参与扩容,对于当前线程的调用方而言是在等待),大大减少了putval等待的概率。