推荐阅读:
概述
本文将对JDK8中 ConcurrentHashMap 源码进行一定程度的解读。解读主要分为六个部分:主要属性与相关内部类介绍、构造函数、put过程、扩容过程、size过程、get过程、与JDK7实现的简单对比。希望对读者学习ConcurrentHashMap有一定的帮助。
阅读本文前,可能需要读者对HashMap和红黑树等有基本的了解。
主要属性和主要的内部类
主要属性
常量
ConcurrentHashMap中常量一共分为以下几个部分:
- 容量相关:MAXIMUM_CAPACITY、DEFAULT_CAPACITY、MAX_ARRAY_SIZE
- 兼容JDK 7而保留的部分常量:DEFAULT_CONCURRENCY_LEVEL、LOAD_FACTOR
- 红黑树升级和退化相关的常量:TREEIFY_THRESHOLD、UNTREEIFY_THRESHOLD、MIN_TREEIFY_CAPACITY
- 扩容相关:MIN_TRANSFER_STRIDE、RESIZE_STAMP_BITS、MAX_RESIZERS、RESIZE_STAMP_SHIFT
- 节点状态常量:MOVED、TREEBIN、RESERVED
/* ---------------- Constants -------------- */
/**
* HashMap的最大容量
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* 数组最大长度
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 默认最大并发等级
*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* 负载因子
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* 链表升级成红黑树的阈值
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树退化成链表的阈值
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 链表升级成树需要满足的最小容量,若不满足,则会先扩容
*/
static final int MIN_TREEIFY_CAPACITY = 64;
//最小转移步长
private static final int MIN_TRANSFER_STRIDE = 16;
//这个常量是用来计算HashMap不同容量有不同的resizeStamp用的
private static int RESIZE_STAMP_BITS = 16;
//最大参与扩容的线程数 相当大的一个数 基本上是不会触及该上线的
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
//要对resizeStamp进行位移运算的一个敞亮
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
//特殊的节点哈希值
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
//获取CPU的数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
其中,因为JDK8中ConcurrentHashMap的实现方式和JDK7的不同,因此DEFAULT_CONCURRENCY_LEVEL已经没有实际作用了。并且在JDK8中,LOAD_FACTOR也已经固定成了0.75f。
另外,MOVED,TREEBIN,RESERVED是用来表示特殊节点的哈希值。该类特殊节点均不含实际元素,且其哈希值被设置为负数和普通节点区分。
剩下的涉及扩容的常量我们在相关的章节中再介绍。
成员变量
通常成员变量都是会负责记录当前类的状态的,ConcurrentHashMap也是如此。因此了解清除成员变量的作用,对我们后续分析ConcurrentHashMap的操作流程很有感帮助。
/* ---------------- Fields -------------- */
/**
* 底层数组
*/
transient volatile Node<K,V>[] table;
//扩容时 使用的另一个数组
private transient volatile Node<K,V>[] nextTable;
//统计size的一部分
private transient volatile long baseCount;
/**
* sizeCtl与table的resize和init有关
* sizeCtl = -1时,表示table正在init
* sizeCtl < 0 且不等于-1时,表示正在resize
* sizeCtl > 0 时,表示下次需要resize的阈值,即capacity * loadfactory
*/
private transient volatile int sizeCtl;
//记录下一次要transfer对应的Index
private transient volatile int transferIndex;
//表示是否有线程正在修改CounterCells
private transient volatile int cellsBusy;
//用来统计size
private transient volatile CounterCell[] counterCells;
// views
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
同样的,成员变量也可以按作用分成几类:
- 用作底层数据结构的实现:Node<K,V>[] table
- 用作统计元素的大小:baseCount、CounterCell[] counterCells、cellsBusy
- 用作记录ConcurrentHashMap的状态:sizeCtl
- 用作扩容记录:Node<K,V>[] nextTable、transferIndex
- 用作转成其它类型的视图:KeySetView<K,V> keySet、ValuesView<K,V> values、EntrySetView<K,V> entrySet
table数组很好理解。因为HashMap的实现是基于数组的,在冲突时通过链地址解决。因此所有的数据都以数组为入口。
另一个数组nextTable是在扩容时备用的。 如果了解Redis的数据结构的读者,应该对这个不陌生,redis渐进式rehash就是通过两个哈希表加一个index实现的。而JDK8中在resize时,也采取了类似的方式(下文我们会介绍到:按步长逐步transfer)。
另外,比较重要的一个属性就是sizeCtl。
如果看过Doug Lea老爷子在JUC下的其他类,经常会有一个特殊的变量表示当前对象的状态。并且已CAS的方式去修改这个变量,实现自旋锁的功能(例如:AQS中的state)。
这里的sizeCtl就是一个富有特殊意义的变量。
当sizeCtl大于0时,表示扩容的阈值(没错,就是HashMap中threshold变量的作用),而且上文我们也了解到在JDK8中由于loadfactor已经被固定为0.75f。因此在正常状态下(非扩容状态),sizeCtl = oldCap >> 1 – (oldCap << 2)。 而sizeCtl == -1是一个特殊的状态标志,表示ConcurrentHashMap正在初始化底层数组。
当sizeCtl为其他负数时,表示ConcurrentHashMap正在进程扩容,其中,高16位可以反应出扩容前数组的大小,而后16位可以反应出此时参与扩容的线程数。
内部类
ConcurrentHashMap拥有大量的内部类,但其中大部分都是用来遍历或是在Fork/Join框架中平行遍历时使用的。这部分类内部类我们不在过多介绍。主要看CountCell和几个Node的类。
CounterCell
首先,CounterCell是用来统计ConcurrentHashMap用的,其内部有个value,用来表示元素个数。size()函数就是通过累加countCells数组中所有CounterCell的value值,再加上BaseCount得到的。相当于ConcurrentHashMap把size这个属性拆散保存在了个多个地方。
Node
同HashMap一样,为了提高链表的遍历速度,ConcurrentHashMap也引用了红黑树。而Node就表示链表中的节点,并且他还是其他节点的父类。
TreeNode
TreeNode表示红黑树中的节点,按照红黑树的标准,它还拥有父节点和左右子节点的属性,此外还需要标识是否为红节点。
TreeBin
TreeBin是一个特殊的节点,用来指向红黑树的根节点,并不存储真实的元素,因此它的节点的哈希值是一个固定的特殊值-2。
ForwardingNode
ForwardingNode和TreeBin一样,并不存储实际元素,而是指向nextTable,哈希值也是一个特殊的固定值(-1)。它在扩容中会使用,表示这个桶上的元素已经迁移到新的数组中去了。
ReservationNode
同样是一个特殊值,在putIfAbsent时使用。因为put时需要对桶上的元素上对象锁(ConcurrentHashMap并非是完全无锁的,只是尽可能少的去使用锁),这时就会添加一个临时占位用的节点ReservationNode。
构造函数
因为构造函数是公有的API,所以必须要和JDK7中保持一致。虽然其中的部分含义可能发生了一些变化。
我们看一下参数最全的构造函数。
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?