1. 概述
ConcurrentHashMap是JDK提供的一种线程安全的HashMap实现,JDK1.8对ConcurrentHashMap进行了大量优化,除了增加了函数式编程特性,还对加锁方式进行了优化,它抛弃了JDK1.6中分段锁的设计,而是直接对Map中Table数组的每个节点进行加锁,进一步减少了锁粒度,并且不再采用ReentrantLock加锁 ,直接使用synchronized同步块(JDK1.6开始已经对synchronized 做了大量优化,加入了自旋锁、偏向锁、轻量级锁、重量级锁等)。
为了提高查询效率,采用了数组+链表+红黑树的设计,当链表中的元素个数大于64,且数组中链表节点长度大于8,则会自动把链表转化为红黑树,当两个条件有一个不满足时,会回退到数组+单链表的数据结构。
在JDK1.8的实现中,还实现了并发扩容机制,也就是可以由多个线程同时帮助扩容,加速数据转移过程,极大提升了效率,本文尝试着把这个扩容过程解释清楚。
2. 实现原理
2.1 基础
首先介绍一下CocurrentHashMap中几个重要变量,这些变量在原子更新、并发扩容控制以及统计元素个数方面发挥着重要作用。
// 代表Map中元素个数的的基础计数器,当无竞争时直接使用CAS方式更新该数值
transient volatile long baseCount;
/**
* sizeCtl用于table初始化和扩容控制,当为正数时表示table的扩容阈值(n * 0.75),当为负数时表示table正在初始化或者扩容,
* -1表示table正在初始化,其他负值代表正在扩容,第一个扩容的线程会把扩容戳rs左移RESIZE_STAMP_SHIFT(默认16)位再加2更新设置到sizeCtl中(sizeCtl = (rs << 16) + 2),
* 每次一个新线程来扩容时都令sizeCtl = sizeCtl + 1,因此可根据sizeCtl计算出正在扩容的线程数,注释中所
* 描述的 sizeCtl = -(1+threads)是不准确的。扩容时sizeCtl有两部分组成,第一部分是扩容戳,占据sizeCtl的高有效位,长度为
* RESIZE_STAMP_BITS位(默认16),剩下的低有效位长度为32-RESIZE_STAMP_BITS位(16),每个新线程协助扩容时sizeCtl+1
* ,直到所有的低有效位被占满,低有效位默认占16位(最高位为符号位),所以扩容线程数默认最大为65535
*/
transient volatile int sizeCtl;
/**
* 用于控制多个线程去扩容时领取扩容子任务,每个线程领取子任务时,要减去扩容步长,如果能减成功,
* 则成功领取一个扩容子任务,`transferIndex = transferIndex - stride(扩容步长)`,transferIndex减到0时
* 代表没有可以领取的扩容子任务。
*/
transient volatile int transferIndex;
// 扩容或者创建CounterCells时使用的自旋锁(使用CAS实现);
transient volatile int cellsBusy;
/**
* 存储Map中元素的计数器,当并发量较高时`baseCount`竞争较为激烈,更新效率较低,所以把变化的数值
* 更新到`counterCells`中的某个节点上,计算size()时需要统计每个`counterCells`的大小再加上`baseCount`的数值。
*/
transient volatile CounterCell[] counterCells;
/**
* ConcurrentHashMap采用cas算法进行更新变量(table[i],sizeCtl,transferIndex,cellsBusy等)来保证线程安全性,它其实是一种乐观策略,
* 假设每次都不会产生冲突,所以能够直接更新成功,如果出现冲突则再重试,直到更新成功。实现cas主要是借助了`sun.misc.Unsafe`类,该类提供了
* 诸多根据内存偏移量直接从内存中读取设置对象属性的底层操作。
*/
static final sun.misc.Unsafe U;
下面是ConcurrentHashMap中的重要常量的含义及功能说明
// HashMap的最大容量(table数组的长度):2^30,因为hashCode最高两位用于控制目的,因此hashCode最大取值为2^30,
// 所以table数组长度n > hashCode,hashCode & (n-1)时无法索引到数组后面的节点上
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 负载因子,最大容量为数组长度*负载因子,元素个数超过容量则触发扩容
private static final float LOAD_FACTOR = 0.75f;
// 链表长度大于8时链表转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 红黑树节点数小于6时红黑树转化为单链表
static final int UNTREEIFY_THRESHOLD = 6;
// 容量大于64时转化为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
// 最小转移步长:由于在扩容过程中,会把一个待转移的数组分为多个区间段(转移步长),每个线程一次转移一个区间段的数据,
// 一个区间段(转移步长)的默认长度是16,实际运行过程中会动态计算
private static final int MIN_TRANSFER_STRIDE = 16;
/**
* 扩容戳有效位数:每次在需要扩容的时会根据当前数组table的大小生成一个扩容戳,当一个线程需要
* 协助扩容时需要实时计算扩容戳来验证是否
* 需要协助扩容或扩容过程是否完成,生成扩容戳的方式:Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
* 其中n表示当前t