基础描述
ConcurrentHashMap和HashMap的功能是基本一样的,ConcurrentHashMap是 HashMap 的线程安全版本,其内部与HashMap类似同样采用了数组(hash桶)链表/红黑树的方式来实现。
线程的安全性如何保障?在HashTable中是直接在 put 和 get 函数上加synchronized关键字,但是这么做锁的粒度太大(整个容器实例)非常影响并发性能。在JDK1.8中,ConcurrentHashMap采用了CAS与synchronized(在必须需要锁的时候仅对hashSlot的Node加synchronized)来实现对容器操作的线程安全保障。同时在ConcurrentHashMap中不允许key与value存储null值。
多并发下如何实现扩容:在ConcurrentHashMap中采用的是分段扩容法,即每个线程负责一段,默认最小是 16,也就是说如果ConcurrentHashMap中只有 16 个槽位,那么就只会有一个线程参与扩容。如果大于16则根据当前CPU数来进行分配,最大参与扩容线程数不会超过CPU数。在ConcurrenthashMap中通过sizeCtl状态来记录何时扩容以及参与扩容的线程个数。
结构定义
ConcurrentHashMap的结构与HashMap没有太多的区别,不同处在于slot中存储Node部分除本身Node的单链表结构外,比HashMap多了三个类型的节点,
分别是:ReservationNode,ForwardingNode,TreeBin.
//ConcurrentHashMap的slot中默认存储数据的节点,单链表,
//===>链表转换成红黑树的条件.
//=>1,static final int TREEIFY_THRESHOLD = 8; 单slot的hash冲突达到8
//=>2,static final int MIN_TREEIFY_CAPACITY = 64;Hash表总长度大于64.
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
//Node的一个子实现,在ConcurrentHashMap.compute系列函数判断记录不存在时
//====>初始插入,用于对节点进行占位.
static final class ReservationNode<K,V> extends Node<K,V> {
ReservationNode() {
super(RESERVED, null, null, null);
}
Node<K,V> find(int h, Object k) {
return null;
}
}
//Node的一个子实现,在ConcurrentHashMap进行扩容时slot的节点.
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
Node<K,V> find(int h, Object k) {
.......
}
}
//Node的一个子实现,当Slot达到变为红黑树条件后,slot中存储的节点.
//==>TreeBin引用一棵具体的红黑树节点TreeNode
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root; //红黑树的根节点
volatile TreeNode<K,V> first;
volatile Thread waiter; //当前写锁的等待线程
//用于红黑树读写锁状态的表示值.
volatile int lockState;
.............
}
//Node的一个子实现,某个slot中红黑树存储的具体记录集
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
........................
}
//ConcurrentHashMap构造函数
//=>private static final int DEFAULT_CAPACITY = 16;默认容量16.
//=>private static final int MAXIMUM_CAPACITY = 1 << 30;最大容量(1073741824).
//注意:HashMap的容量值必须是2的幂
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
ConcurrentHashMap中记录与扩容相关的几个参数定义:
//扩容戳,默认值16,
private static int RESIZE_STAMP_BITS = 16;
//对hash表数组进行扩容时,最大可扩容的线程数量
//值(65535),二进制:0000 0000 0000 0000 1111 1111 1111 1111
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
//扩容戳的位移位数,
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
//(-1):表示hash表开始扩容,
//(-N):表示hash表正在扩容(低16位记录扩容的线程数,高16位记录扩容戳),
//(正数):表示当前hash表容量的0.75(即当size达到0.75时需要扩容)
private transient volatile int sizeCtl;
//hash表扩容时,单个线程处理hash槽位的歩福(即单个线程最小处理16个节点的迁移)
private static final int MIN_TRANSFER_STRIDE = 16;
初始化hash表
ConcurrentHashMap的初始化通过initTable函数来实现,通过对sizeCtl的判断与CAS自旋操作来判断是否抢占到资源,如果sizeCtl小于0,说明其它线程正在进行初始化操作(让出CPU执行时间让其它线程处理),否则尝试通过CAS设置sizeCtl的值为-1,如果能成功设置为-1说明当前线程抢占到资源,对table进行初始化,在初始化时如果sizeCtl的值是大于0的值说明在构建ConcurrentHashMap实例时有配置初始化table容量大小,根据此值来初始化,否则按默认的DEFAULT_CAPACITY(16)来初始化table.
//sizeCtl变量:
//=>1, -1表示hash表正在初始化,
//=>2, 小于负1表示hash表正在扩容(高16位记录扩容戳,低16位记录正在扩容的线程数)
//=>3, 0表示初始状态.
//=>4, 大于0的值,说明当存储记录达到这个值时需要扩容(当前容量的0.75).
private transient volatile int sizeCtl;
//初始化hash表,通过CAS自旋锁来判断是否能成功设置sizeCtl的值为-1,(成功表示当前线程抢到资源)
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//在hashTable还未初始化完成前,一直迭代(成功初始化或被其它线程初始化成功结束).
while ((tab = table) == null || tab.length == 0) {
//如果sizeCtl小于0,说明有其它线程正在进行hash表的初始化或者扩容操作,
//==>此时当前线程应该让出CPU的执行时间给其它线程有充足的时间完成初始化.
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//这里表示没有其它线程竞争hash表的初始化操作,CAS设置sizeCtl的值为-1.
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
//在table为null时,sizeCtl如果大于0就是初始化table的容量,否则就是默认的16个.
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//scizeCtl = table.lenth -(table.length / 4)即:当前table容量的3/4
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
插入数据(put)
1,put主体流程
在ConccurrentHashMap中实现插入与HashMap类似,不同处在于考虑多线程的场景下的插入与size计数的实现上要复杂一些,插入操作具体包含:
1,先判断hashTable是否已经初始化,如果没有,先初始化hash表,并重新刷新table进行判断。
2,对key进行hash判断key对应hashTable的slot,如果slot位置为null时,直接CAS设置hashTable中此slot为一个根据记录(key,value)生成的Node节点作为链表的root节点.
3,如果slot已经存在节点记录,同时节点的hash值为MOVED(-1)说明hashTable正在扩容,执行helpTransfer函数帮助hashTable快速完成扩容迁移操作,在进入helpTransfer时如果扩容完成返回值是hashmap.table数组,否则返回hashmap.nextTable数组。
4,slot当前存储有Node节点,节点类型是链表节点(hash >= 0),迭代到链表的尾部插入当前记录(或者已经存在,替换原Node的值).
5,slot当前存储有Node节点,节点类型是TreeBin,(hash == -2),说明当前slot是红黑树存储,调用TreeBin.putTreeVal实现记录的插入或替换。
6,判断slot中存储的链表数量是否达到转换为红黑树存储的条件(nodeSize >= 8),如果达到转换成红黑树存储(如果hashTable的容量小于64时,会先扩容而不是转换红黑树)。
7,addCount对map的size记录加1,并判断是否需要扩容.
下面分析一下插入数据(put)的主体流程。
分析前,先看看ConcurrentHashMap中如何获取到hashTable中某个slot位置的node节点。
获取hashTable中某个slot位置的节点由tabAt函数实现。
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
//"((long)i << ASHIFT)" 相当于"scale * i",
//通过Unsafe获取到hashTable中指定下标的Node节点.
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
//Unsafe中关于Node数组操作的几个定义
Class<?> ak = Node[].class;
//获取取Node[]数组的内存地址偏移量
ABASE = U.arrayBaseOffset(ak);
//这里获取到数组每个元素的歩长(元素寻址的转换因子).
int scale = U.arrayIndexScale(ak);
//获取到每个元素寻址的左移偏移量(如scala是4(100),那么这里得到的结果是28)
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
pubVal的具体执行流程:
//ConcurrentHashMap中实现数据的插入
//对外暴露的put函数,其直接调用putVal函数来完成数据插入.
public V put(K key, V value) {
return putVal(key, value, false);
}
//具体实现数据插入操作的核心流程,onlyIfAbsent:true表示存在不替换旧值,默认为false
final V putVal(K key, V value, boolean onlyIfAbsent) {
//在ConcurrentHashMap中,插入数据的key与value不能为null.
if (key == null || value == null) throw new NullPointerException();
//对key的hash值,spread函数:"(hash ^ (hash >>> 16)) & 0x7fffffff"
//=>把hash值的低16位与高16位进行异或计算然后与Integer.maxValue按位与.
//=>这里确保了key.hashCode的值非负数,
//=>同时因为hashTable的容量是2的N次方,进行XOR运算可有效减少hash冲突.
int hash = spread(key.hashCode());
int binCount = 0;
//死循环,自旋,直到插入成功结束.
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果当前hashTable还未初始化,先对hashTable进行初始化(initTable)
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//获取hash取模后对应table中slot的Node节点,并赋值给临时变量f,
//"f == null",直接CAS设置table数组对应slot,生成一个新的Node节点,成功后退出迭代.
//"(n - 1) & hash" 用hash值的低位与数组长度减一按位与(其实就是取模)
//"tabAt函数",获取table数组中指定下标的Node节点(保证可见性),(通过Unsafe来读取数组地址偏移量)
//====> Unsafe来读取数组地址偏移量,可参考此函数去实现.
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//CAS设置对应slot为一个新生成的Node节点.因为slot没有存储任何节点,当前node就是链表的root节点.
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果当前slot中root节点的hash值是"MOVED(-1)",说明节点正在进行迁移,
//==>执行helpTransfer函数,帮助迁移,"(fh == MOVED(-1))"表示节点正在迁移.
//==>helpTransfer函数返回值:
//====>1,如果进入函数时扩容已经完成,直接返回map.table数组
//====>2,否则参与扩容(扩容线程未达上限),并返回map.nextTable数组.
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//"synchronized"关键字对slot中rootNode节点实现局部加锁,
synchronized (f) {
//"(tabAt(tab, i) == f)"
//=>刷新slot中最新的root节点,检查是否变化(如链表转成了红黑树就需要重新迭代)
if (tabAt(tab, i) == f) {
//"(fh >= 0)" 表示当前slot是链表结构存储.
if (fh >= 0) {
binCount = 1;
//从链表的root节点(f)开始,向下查找到节点插入的位置并插入节点.
for (Node<K,V> e = f;; ++binCount) {
K ek; //当前迭代的节点(e)的key值.
//如果记录值存在,(key and hash相同),替换节点的value并退出迭代(内层迭代).
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//next为null时,表示链表尾部,在链表尾部插入当前记录并退出迭代(内层迭代).
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,value, null);
break;
}
}
}
//"(fh == TREEBIN(-2))" 表示红黑树节点,红黑树存储时slot位置存储的是TreeBin实例
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//通过TreeBin.putTreeVal向红黑树插入记录.
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//释放锁,因为插入动作已经完成.
if (binCount != 0) {
//判断当前slot中链表的存储记录集是否达到TREEIFY_THRESHOLD(8),
//如果slot链表记录达到8个,并table.length小于MIN_TREEIFY_CAPACITY(64),扩容.
//否则:对当前slot进行存储结构转换,把链表转换成红黑树.
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//流程执行这里说明记录是一条新插入的记录,返回值为null(没有oldValue)
//"addCount" 此函数用于设置map的size计数(真实记录数),
//===>并根据slot的hash冲突判断是否需要对table进行扩容.
addCount(1L, binCount);
return null;
}
//对hash值的低16位与高16位进行XOR运算,并调整hash值为正数.
static final int spread(int h) {
//"(h >>> 16)" 把hash值的高16位移动到低16位处.
//"(h ^ (h >>> 16))"对低16位与高16位进行XOR运算。
//"& HASH_BITS" 确保hash值是正数.
return (h ^ (h >>> 16)) & HASH_BITS;
}
2,插入(红黑树)
在ConcurrentHashMap中与HashMap一样,当hashTable中某个slot的冲突达到8个同时hashTable的容量达到64个或以上,slot中存储记录的链表需要转换为红黑树结构存储,因此这里设计到一个从链表转换为红黑树,同时当slot存储的节点代表红黑树节点时,数据的插入也是向红黑树插入。这里主要分为三个部分:
1,链表结构转换为红黑树结构.
2,红黑树节点插入后树的平衡调整.
3,如果插入记录时slot对应的位置是红黑树,向红黑树插入节点.
a,treeifyBin(链表转红黑树)
treeifyBin函数的执行条件是:当前插入的slot中链表存储结构的数量达到8个.在slot插入记录后,链表数量达到8时(由TREEIFY_THRESHOLD控制),触发treeifyBin来进行处理(如下)。
在ConcurrentHahsMap中,slot位置如果是红黑树时存储的并不是TreeNode的root节点,而是一个TreeBin节点,此节点的hash值为-2(TREEBIN),TreeBin节点中维护有红黑树root节点的指针。
static final int TREEIFY_THRESHOLD = 8;
//putVal函数判断是否转换成红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
接下来分析一下treeifyBin函数的具体实现:
在treeifyBin中,如果hashTable的容量小于64时,会通过tryPresize函数对hashTable进行扩容而不是把链表转换为红黑树,只有当hashTable的容量达到64个,同时单slot的hash冲突达到8个时,才会把链表转换为红黑树并把slot位置存储为一个TreeBin节点。
//当链表存储记录值达到8个时,将链表结构转换为红黑树结构存储.
//通过synchronized关键字对slot中存储的节点进行加锁来实现线程安全.
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//如果当前hashTable的容量小于64时,说明当前容量本身太低导致了hash冲突,先扩容.
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
//获取到指定index在table中存储的node节点(同时节点必须是链表类型节点).
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
//对index的root节点(b)进行加锁,
synchronized (b) {
//重新刷新节点,检查index位置链表的root节点是否发生变化.
if (tabAt(tab, index) == b) {
//顺序从root节点迭代链表,依次添加到红黑树中(此时红黑树特性不满足).
//节点(hd)是链表的root节点,节点(tl)是当前迭代节点(p)的前继节点
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
//生成一个TreeBin节点(调整红黑树结构使其满足红黑树特性)重新放回到index的位置.
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}//退出同步块
}
}
}
TreeBin的构造函数:
此函数作用除了构造一个存储在hashTable对应slot位置的节点外,还需要负责在TreeNode初始化时重新调整树的结构,使其满足红黑树的特性。插入红黑树节点后的平衡调整由balanceInsertion函数实现。
//把一个链表形式的红黑树进行调整并使其满足红黑树的特性.
TreeBin(TreeNode<K,V> b) {
//TreeBin节点的hash值为TREEBIN(-2)
super(TREEBIN, null, null, null);
//把链表的root节点设置为TreeBin的first节点.
this.first = b;
//红黑树的root节点.
TreeNode<K,V> r = null;
//从链表头节点开始迭代链表,构建红黑树
for (TreeNode<K,V> x = b, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
//step1,第一次迭代先把第一个节点设置为红黑树的root节点(颜色为黑色)
if (r == null) {
x.parent = null;
x.red = false;
r = x;
}
//step2,红黑树已经构建root节点,继续迭代后续后续节点向红黑树插入节点.
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
//向红黑树中插入节点(x),要实现插入操作,需要从root开始向左或向右查找插入位置.
for (TreeNode<K,V> p = r;;) {
int dir, ph;
K pk = p.key;
//先得到节点(x要插入的方向)
//=>1,先比较插入节点(x)的hash与树当前节点(p)的hash值,
//=>2,如果hash相同,再比较 插入节点(x)的key与树当前节点(p)的key值
//=>3,如果key(comparable)也相同,通过"System.identityHashCode"比较两个对象的hash值.
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
//"(p = (dir <= 0) ? p.left : p.right)"
//==>根据插入方向(dir)开始让节点(p)向左或向右移动.
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//如果当前迭代节点(p)已经是树的叶节点,就在这个位置插入节点..
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//插入节点成功,调整树的平衡,得到调整后的新root节点(r).
r = balanceInsertion(r, x);
break;
}
}
}
}
//设置TreeBin对root节点的引用.
this.root = r;
assert checkInvariants(root);
}
b.balanceInsertion(平衡调整)
在红黑树节点插入时进行平衡调整总共有4个分支,其中两个分支是红黑树不超过二层的情况下,这种情况比较简单(见下面代码的step2与step3),另外两个分支每个分支对应三种场景,其中step5与step4是相对称的操作。
//红黑树节点插入后的树平衡处理,传入参数:root表示当前树的根节点,x表示当前插入节点.
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
//step1,插入节点默认是红色.
x.red = true;
//xp=x.parent,xpp=xp.parent,xppl=xp.parent.left,xppr=xp.parent.right
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//step2,如果关注节点(x)没有父节点,直接设置节点为黑色并返回此节点(root).
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//step3