构造方法
总共有五种构造器,总结: 两个重要参数 initalCapacity 初始化容量;loadFactor 扩容的负载因子(用于计算扩容阈值);默认初始化容量为16;默认负载因子为0.75。
一个BUG:一个参数构造器和三参构造器构造的集合对象长度不一致;一个参数的构造器即使在构造时,给定初始化容量是2的n次幂,结果也会被改变;在JDK12中得以修正。
/**
* initialCapacity 集合数组初始化容量
* loadFactor 负载因子(用于计算扩容阈值)
* concurrencyLevel 估计的并发更新线程数。实现可以使用这个值作为大小提示
*/
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
/**
* 其实就一句话,负载因子必须是大于 0 的数字;初始化容量不能 < 0; 预计并发更新线程数必须 > 0
*/
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 如果初始化容量小于预计更新线程数,初始化容量就时更新线程数
if (initialCapacity < concurrencyLevel)
initialCapacity = concurrencyLevel;
// 预计初始化集合的大小;1.0 保证不会等于0(因为初始化容量可以为0)
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
/**
* 真正集合大小的容量,分为两种情况:
* 1、预计容量 >= 数组最大容量,采用数组最大容量(1 << 30;大约十亿多)
* 2、预计容量 < 数组最大容量,采用最紧急size的2的n次幂
*/
int cap = (size >= (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size);
// 将数组真正的初始化容量赋值给成员变量 sizeCtl
this.sizeCtl = cap;
}
// 本质调用三参构造器
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
/**
* BUG(两参本质调用三参)三个参数的构造器创建的对象的初始化数组长度不一致
*
* 正确的应该是 JDK12中修正如下:
* public ConcurrentHashMap() {
* this(initialCapacity, LOAD_FACTOR, 1);
* }
*
* 举例 16 负载因子采用和默认一致的 0.75
* tableSizeFor(16 + (16 >>> 1) + 1) = 32 ; 16 + (16 >>> 1) + 1 =25 ,最接近25的2^n幂的结果就是32
* 一个参数 cap = (16 >= (1 << 30) ? 1 << 30 : 32) = 32;
*
* size = 1 + 16/0.75 = 13;
* tableSizeFor(13) =16 ,最接近13的2^n幂的结果就是16
* 三个参数 cap = (13 >= 1 << 30 ? 1 << 30 : 16) = 16;
*
* 实际应该是16
*/
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;
}
// 参数为Map对象,采用默认初始化容量,不足时再扩容
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
// 默认初始化容量 16
this.sizeCtl = DEFAULT_CAPACITY;
// 容量不足时,会在putAll 中执行 tryPreSize 尝试扩容
putAll(m);
}
// 无参构造器 什么都没做
public ConcurrentHashMap() {
}
put方法
put方法的执行过程
1、判断key,value值是否为空,为空抛出空指针异常;
2、判断集合数组是否初始化,未初始化进行初始化;
3、用key的hash值与数组长度做 & 运算得出索引位置:hash & (n -1),并判断索引位置的节点是否为空,为空采用CAS放入数据
4、索引位置不为空分为三种情况
4.1 数组正在扩容,数据在迁移中,索引位置的节点为forward节点,forward的节点特点hash值为-1,当前线程进行协助扩容
----- 4.2和4.3前置:所以位置不管是链表还是红黑树,都先加同步锁synchronized
4.2 索引位置为链表,遍历链表,查看链表是否有相同的key存在,
– 有相同的key根据onlyIfAbsent的值进行覆盖或者保持不变;
– 没有相同的key,找到链表尾部插入数据。
4.3 索引位置为红黑树,调用TreeBin.putTreeNode插入红黑树。
4.5 如果链表长度超过8,尝试树化。(不一定会转为红黑树,转为红黑树还有一个条件,数组长度 > 64)
注意:在ConcurrentHashMap中,hash值负数有特殊含义,与HashMap不同的地方在于计算hash值时,hash & HASH_BITS,保证正常的hash值是一个正数;
HASH_BITS = 0x7fffffff;
HASH_BITS = 011111111 11111111 11111111 11111;最高位为0,保证与hash值做 & 运算后,得到一个最高位为0的值。
当Hash值负数的三种情况:
当前位置正在进行扩容
static final int MOVED = -1;
当前位置下是一棵红黑树
static final int TREEBIN = -2;
预留当前位置 ====> computIfAbsent ====> new ReservationNode<K,V>();
static final int RESERVED = -3;
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
public V put(K key, V value) {
/**
* onlyIfAbsent
* 值为false时,key存在覆盖value,key不存在,正常插入;
* 值true时,key存在,什么的都不做,value保持原来的不变,key不存在,正常插入数据。
*/
return putVal(key, value, false);
}
/**
* 1、保证插入key,value都不为空,为空抛出异常直接结束方法
* 2、判断数组是否初始化,没有初始化,初始化数组
* 3、数组已经初始化,根据hash & n - 1 计算出在数组中的插入位置,并判断当前位置是否为null,为null,直接放入数组
* 4、当前位置不为空,查看节点是否是正在扩容的节点 fwd( hash = -1,nextTable),如果是扩容节点,则协助扩容
* 5、当前位置不为空,且当前节点不是扩容节点,给当前位置的节点加锁
* 5.1 当前位置下是链表,两种情况,hash >= 0
* -----链表中存在相同的key,根据onlyIfAbsent 判断是(false)覆盖原有value,(true)还是保持原有value不变
* -----链表中没有相同的key,找到链表尾部插入数据
* 5.2 hash == -2;说明当位置中是一颗红黑树
*
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key和value都不允许为空,跟HashMap的区别
if (key == null || value == null) throw new NullPointerException();
/**
* 计算hash值
*/
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K, V>[] tab = table;;) {
/**
* f 当前索引位置的节点数据
* n 当前数组长度
* i 当前要插入数据的索引位置
* fh 当前索引位置i上的节点的hash值
*/
Node<K, V> f; int n, i, fh;
// 判断数组是否初始化
if (tab == null || (n = tab.length) == 0)
// 没有初始化,初始化数组
tab = initTable();
/**
* 判断当前要插入数据的索引位置是否为空
* tabAt 在内存根据偏移量取出数据并使用volatile加载,保证可见性
*/
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 索引位置为空,则通过CAS插入数据
if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null)))
// 插入成功,跳出当前循环
break;
}
// 当前位置是否正在扩容
else if ((fh = f.hash) == MOVED)
// 当前插入数据的线程协助扩容
tab = helpTransfer(tab, f);
// 当前索引位置下有数据,分为两种情况,当前索引位置下是链表或者一颗红黑树
else {
// 链表中当前节点的value
V oldVal = null;
// 基于当前索引位置的节点数据进行加锁,也就是常说的桶锁。
synchronized (f) {
// 再次判断当前位置引用是否变更,引用变更说明发生了并发冲突
if (tabAt(tab, i) == f) {
//当前位置中是一个链表
if (fh >= 0) {
// 在链表中位置的索引,1表示链表第二个位置
//(数组索引为i的节点为链表首节点 ==> 0)
binCount = 1;
for (Node<K, V> e = f;; ++binCount) {
// 当前位置i下的链表位置为binCount位置的节点的key值。
K ek;
/**
* 判断链表中是否有相同的key,
* hash值相等,且引用地址相等 或 equals相等
*/
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 将链表当前节点的value赋值给oldValue
oldVal = e.val;
/**
* 是否覆盖当前链表当前位置的value属性值
* onlyIfAbsent
* 值为false时,key存在覆盖value,key不存在,正常插入;
* 值true时,key存在,什么的都不做,value保持原来的不变
* key不存在,正常插入数据。
*/
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K, V> pred = e;
// 找到链表尾部(链表尾部的next为空)
if ((e = e.next) == null) {
/**
* 根据当前插入key,value创建节点,
* 并将新节点追加到链表尾部
*/
pred.next = new Node<K, V>(hash, key, value, null);
break;
}
}
}
// 当前 i = hash & n -1 位置下是一颗红黑树
else if (f instanceof TreeBin) {
// 临时节点:用于接收插入红黑树后的返回值
Node<K, V> p;
binCount = 2;
// 插入到当前节点下红黑树中(因为当前节点是红黑树节点,所以可以强转)
// p 不为空,说明红黑树中有相同的key存在
if ((p = ((TreeBin<K, V>)f).putTreeVal(hash, key, value)) != null) {
// 获取要覆盖节点的原来的value
oldVal = p.val;
// 是否允许覆盖旧值
if (!onlyIfAbsent)
// 覆盖旧值
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 树化
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
初始化集合数组-initTable
初始化数组的执行过程
1、while循环判断数组是否初始化,为空或者数组长度为空0,表示数组没有初始化
1.1、已经初始化直接返回当前数组
1.2、没有初始化,分为以下两种情况:
----- sizeCtl < 0,当前线程让步,进入就绪状态。
----- 否则,尝试设置 sizeCtl 为 -1,设置不成功继续while循环,设置成功,再次判断是数组引用是否变更,是否初始化。没有初始化进行初始化,并将sizeCtl设置为下次扩容的阈值。
sizeCtl > 0 表示数组的初始化长度或者扩容的阈值
sizeCtl = 0 表示是数组没有初始化
sizeCtl = -1 表示数组正在初始化
sizeCtl < -1 表示数组正在扩容的线程数,-2代表有一个线程扩容,以此类推
/**
* 初始化数组
*
* ----- 如果是用空构造器创建的对象,sizeCtl = 0;
* ----- 如果是用有参的构造器创建的对象,sizeCtl = 数组初始化长度(map为参数的,采用的是数组默认长度)
*
* 1、判断是否已经初始化
* 2、如果没有初始化,判断是否正在初始化,正在初始化时,改变当前线程状态 running --> runnable, ?
* 3、没有初始化,且不在初始化中,CAS 设置sizeCtl为-1,尝试初始化
* 4、CAS 成功后再次判断数组是否初始化,没有初始化进行初始化,初始化完成后,将sizeCtl 设置为下次扩容的阈值
*/
private final Node<K, V>[] initTable() {
// tab 当前数组,sc 当前sizeCtl的值
Node<K, V>[] tab; int sc;
// 再次判断数组是否初始化
while ((tab = table) == null || tab.length == 0) {
/**
* sizeCtl 数组初始化和扩容操作时的变量
* 值为-1,当前数组正在初始化
* 小于-1,当前数组正在扩容的线程个数(-2一个线程正在扩容,-3 两个线程正在扩容...)
* 值为0,没有初始化
* 值大于0,代表当前数组的扩容阈值,或者当前数组初始化大小
*
*/
// 判断数组是否正在初始化或者扩容
if ((sc = sizeCtl) < 0)
// 当前线程让步(让出CPU执行),进入就绪状态
Thread.yield(); // lost initialization race; just spin
// 线程以CAS的方式,设置sc的值为-1,只有一个线程能设置成功
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 再次判断数组是否初始化
if ((tab = table) == null || tab.length == 0) {
// 如果sc > 0 ,那么sc是数组初始化长度,否则数组长度为默认数组初始化长度==》16。
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 真正开始初始化数组
Node<K, V>[] nt = (Node<K, V>[])new Node<?, ?>[n];
// 将初始化后的数组赋值给局部变量tab和全局变量table
table = tab = nt;
// 下次数组扩容的阈值 3/4 = 0.75
sc = n - (n >>> 2);
}
} finally {
// 将sc (扩容阈值)赋值给全局变量sizeCtl
sizeCtl = sc;
}
break;
}
}
return tab;
}
插入红黑树
/**
* 插入数据到红黑树
*
* 与链表转红黑树类似
*/
final TreeNode<K, V> putTreeVal(int h, K k, V v) {
// key 的类对象
Class<?> kc = null;
// 是否已经搜索过子节点
boolean searched = false;
// p 父节点的临时变量(for循环的起点是根节点)
for (TreeNode<K, V> p = root;;) {
/**
* dir 判断当前节点应该插入左子树还是右子树;左:dir = -1;右:dir=1;
* ph 父节点的hash值
* pk 父节点的key值
*
*/
int dir, ph; K pk;
// 如果父节点为空,说明树不存在
if (p == null) {
/**
* 以当前插入节点为红黑树的根节点
* 以当前插入节点为红黑树中链表的头节点
* 结束循环(说明目前只有当前插入节点一个节点,没必要循环了)
*/
first = root = new TreeNode<K, V>(h, k, v, null, null);
break;
// 如果父节点不为空,判断父节点hash 是否大于 当前插入节点的hash值
} else if ((ph = p.hash) > h)
// 如果父hash > hash,当前要插入的节点应该插入左子树
dir = -1;
else if (ph < h)
// 如果父hash < hash,当前要插入的节点应该插入右子树
dir = 1;
// 如果当前插入key 与 父节点key引用地址相同 或者 key equals相等
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
// 说父节点的key与当前插入key相同,直接返回当前节点
return p;
/**
* 如果引用地址不等且equals不等,key实现了Comparable,且使用compareTo进行比较 k 与 pk相等时
* 搜索子节点
* kc = comparableClassFor(k) key的类是否实现了Comparable,实现了返回key的Class对象,否则为空
*/
else if ((kc == null && (kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// 只会搜索一次,搜索一次后 searched被赋值为true,表示搜索过了
if (!searched) {
TreeNode<K, V> q, ch;
searched = true;
/**
* 搜索子树,如果子树中有相同的key,结束方法,并返回相同key的节点
* 结束方法,并返回相同key的节点
*
* ((ch = p.left) != null && (q = ch.findTreeNode(h, k, kc)) != null)
* 左子树不为空,搜索左子树找到key相同的节点返回节点,否则返回null
*
* ((ch = p.right) != null &&(q = ch.findTreeNode(h, k, kc)) != null)
* 右子树不为空,搜索右子树找到key相同的节点返回节点,否则返回null
*/
if (((ch = p.left) != null && (q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null && (q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
// k hash值 <= pk的hash值 ? -1 : 1
dir = tieBreakOrder(k, pk);
}
// 根据dir,判断放入左或右子树,并维护链表
TreeNode<K, V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 将根节点赋值给临时变量f;x 当前节点的临时变量
TreeNode<K, V> x, f = first;
// 将当前节点赋值给链表头节点
first = x = new TreeNode<K, V>(h, k, v, f, xp);
// 如果根节点不为空,将当前节点赋值给根节点的prev(链表的上一个节点)
if (f != null)
f.prev = x;
// 将当前节点放入父节点的左子树
if (dir <= 0)
xp.left = x;
// 将当前节点放入父节点的左子树
else
xp.right = x;
// 如果父节点不是红色,则当前节点为红色
if (!xp.red)
x.red = true;
// 如果父节点是红色,则加锁,进行自平衡
else {
lockRoot();
try {
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
}
break;
}
}
// 检查红黑树
assert checkInvariants(root);
return null;
}
维护红黑树平衡及特性
/**
* 红黑树做自平衡,以及保证红黑树的特性的操作。红黑树的特性;
* ps:为了方便说明,将特性给予编号:
* -- 1、每个节点非红即黑。
* -- 2、根节点时黑色的。
* -- 3、如果当前节点是红色的,它的子节点一定是黑色的
* -- 4、叶子节点也一定是黑色的(?)
* -- 5、从根节点出发到叶子节点路径中,黑节点数量是相同的
*
*
* root:当前做自平衡操作时的根节点,x:当前节点
*/
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root,
TreeNode<K, V> x) {
// 默认插入节点时红色的
x.red = true;
/**
* 这一部分挺好玩的 ;x:当前节点 p父节点
*
* xp:当前节点的父节点
* xpp:爷爷节点(当前节点的父节点的父节点 ps:爸爸的爸爸叫什么?)
* xppl: 爷爷节点的左节点(这是爷爷的小儿子:左hash > 父hash)
* xppl: 爷爷节点的右节点(这是爷爷的大儿子:右hash < 父hash)
*/
for (TreeNode<K, V> xp, xpp, xppl, xppr;;) {
/**
* 如果当前节点的父节点为空,则说明当前节点是根节点,根据红黑树特性2,将当前节点标记为黑色;
*
* 如果当前节点是根节点,因为是插入,所以说明当前直插入了一个节点,直接结束方法
*/
if ((xp = x.parent) == null) {
x.red = false;
return x;
// 如果父节点是黑色的,或者爷爷节点是空,则说明不需要做自平衡,直接结束方法
} else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 左子树操作,当前节点的父节点位于左子树
if (xp == (xppl = xpp.left)) {
// 如果叔叔节点不为空,且是红色
if ((xppr = xpp.right) != null && xppr.red) {
// 将叔叔节点,父节点都变为黑色;爷爷节点变为红色
xppr.red = false;
xp.red = false;
xpp.red = true;
// 将当前节点指向爷爷节点,再走一次循环(验证爷爷节点是否满足特性)
x = xpp;
// 叔叔节点为空或者是黑色的
} else {
// 当前节点是位于父节点右边的节点
if (x == xp.right) {
// 父节点进行左旋操作
root = rotateLeft(root, x = xp);
// 判断父节点是否为空,为空,则爷爷节点为空;不为空,则爷爷节点是父节点的父节点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 父节点不为空
if (xp != null) {
// 将父节点变为黑色
xp.red = false;
// 如果爷爷节点不为空,将爷爷节点变为红色,并将爷爷节点右旋
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
// 右子树操作,当前节点的父节点位于右子树(与左子树类似)
} else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
} else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
左旋操作:
右旋操作:
红黑树平衡时涉及到的线程读写锁
TreeBin的锁操作,没有基于AQS,仅仅是对一个变量的CAS操作和一些业务判断实现的。
每次读线程操作,对lockState+4。
写线程操作,对lockState 设置为 1,如果读操作占用着线程,就先设置 lockState为 2,表示当前有写线程在等待写锁
// TreeBin的锁操作
// 如果说有读线程在读取红黑树的数据,这时,写线程要阻塞(做平衡前)
// 如果有写线程正在操作红黑树(做平衡),读线程不会阻塞,会读取双向链表
// 读读不会阻塞!
static final class TreeBin<K,V> extends Node<K,V> {
// waiter:等待获取写锁的线程
volatile Thread waiter;
// lockState:当前TreeBin的锁状态
volatile int lockState;
// 对锁状态进行运算的值
// 有线程拿着写锁
static final int WRITER = 1;
// 有写线程,再等待获取写锁
static final int WAITER = 2;
// 读线程,在红黑树中检索时,需要先对lockState + READER
// 这个只会在读操作中遇到
static final int READER = 4;
// 加锁-写锁
private final void lockRoot() {
// 将lockState从0设置为1,代表拿到写锁成功
if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
// 如果写锁没拿到,执行contendedLock
contendedLock();
}
// 释放写锁
private final void unlockRoot() {
lockState = 0;
}
// 写线程没有拿到写锁,执行当前方法
private final void contendedLock() {
// 是否有线程正在等待
boolean waiting = false;
// 死循环,s是lockState的临时变量
for (int s;;) {
//
// lockState & 11111101 ,只要结果为0,说明当前写锁,和读锁都没线程获取
if (((s = lockState) & ~WAITER) == 0) {
// CAS一波,尝试将lockState再次修改为1,
if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
// 成功拿到锁资源,并判断是否在waiting
if (waiting)
// 如果当前线程挂起过,直接将之前等待的线程资源设置为null
waiter = null;
return;
}
}
// 有读操作在占用资源
// lockState & 00000010,代表当前没有写操作挂起等待。
else if ((s & WAITER) == 0) {
// 基于CAS,将LOCKSTATE的第二位设置为1
if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
// 如果成功,代表当前线程可以waiting等待了
waiting = true;
waiter = Thread.currentThread();
}
}
else if (waiting)
// 挂起当前线程!会由写操作唤醒
LockSupport.park(this);
}
}
}