public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V> {
/**
* 数组的最大容量
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 数组初始化的默认容量,必须是2的n次方
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* 数组的最大size
*/
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;
/**
* 当桶(bucket)上的结点数大于8时会转成红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当桶(bucket)上的结点数小于6时树转链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 桶中结构转化为红黑树对应的数组长度最小的值
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 每次进行转移的最小值
*/
private static final int MIN_TRANSFER_STRIDE = 16;
/**
* 生成sizeCtl所使用的bit位数
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* 进行扩容时的最大线程数
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/**
* 记录sizeCtl中的大小所需要进行的偏移位数
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// 0:默认为 0,代表数组还未初始化,且数组的初始化容量为16
// 负数:当数组正在初始化时, 为 -1
// 负数:当数组正在扩容时, 为 -(1 + 扩容线程数)
// 正数:当初始化还未初始化时代表数组的初始容量,当数组初始化后为数组扩容的阈值大小:数组的初始容量*0.75
private transient volatile int sizeCtl;
static final int MOVED = -1; // 表示正在转移
static final int TREEBIN = -2; // 表示已经转换成树
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
transient volatile Node<K,V>[] table;//默认没初始化的数组,用来保存元素
private transient volatile Node<K,V>[] nextTable;//转移的时候用的数组
}
实际使用时,我们一般不使用空参数构造函数,调用带参构造方法,指定一个初始容量,因为如果不指定初始容量的话,他会在第一次调用put()方法时,对数组容量进行扩容并初始化,扩容比较影响性能。
/**
* Creates a new, empty map with the default initial table size (16).
* 不指定数组容量,扩容时扩容到默认数组容量为16
*/
public ConcurrentHashMap() {
}
如果指定的初始容量不是2的n次方,会调用 tableSizeFor(int c)方法,将数组的容量增加至2的n次方:与HashMap不同的是,如果我们传入的容量为16,那么返回的容量也为16,但是ConcurrentHashMap1.8中,如果传入的容量为16,返回的容量为32
/**
* Creates a new, empty map with an initial table size
*/
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0) throw new IllegalArgumentException();
//将输入的数组容量增加至2的n次方
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
//在HashMap中是tableSizeFor(initialCapacity),如果传入16,返回仍然是16
//注意:在这里,如果传入16,返回的是32
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
//如果在实例化对象的时候指定了容量,则初始化sizeCtl
this.sizeCtl = cap;
}
/**
* Returns a power of two table size for the given desired capacity.
*/
private static final int tableSizeFor(int c) {
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
ConcurrentHashMap能够保证线程安全的向集合中添加元素:
① 对数组进行初始化时,使用CAS+自旋的方式,初始化数组
② 如果要添加元素的桶位处没有元素,那么使用CAS+自旋的方式向桶位中添加元素
③ 如果要添加元素的桶位中有元素,那么使用Synchronized给当前桶位加锁,锁对象为桶位中的头节点对象,这样 可以线程安全的向桶中添加元素。
put方法流程总结:
① 添加数据时,首先判断当前数组是否为null,如果为null ,使用CAS+自旋的方式,初始化数组。
② 如果数组不为null,判断桶中元素的key的hash值是否为-1,如果为-1,说明当前数组正在扩容,即将旧数组中的元素复制到新数组中,当前线程帮助数组扩容。
③ 如果数组不为null,并且数组也不再扩容,根据hash值计算要输入数据在数组中的桶下标,取出桶中的元素,并判断是否为null,如果为null,那么采用CAS+自旋的方式线程安全的将当前节点添加到桶中。
④ 如果桶中的元素不为null,那么使用Synchronized对当前桶中加锁,即桶对象为当前桶的头节点对象,如果其他线程想要向这个桶中加锁,需要先获的这个锁对象,可以保证向这个桶中添加节点元素时线程安全的。
⑤ 判断桶中元素的节点Key的hash值是否小于0,如果小于说明是链表,那么遍历链表,判断链表中是否存在于要添加元素的key重复的,如果有键相同值覆盖,如果没有直接添加到链表的尾部;如果不是链表节点判断是否是红黑树节点,如果是的话,调用红黑树添加节点的方法。
⑥ 添加完节点后,判断在该桶中添加的节点数是否大于8,如果大于8,将链表转换为红黑树。
⑦ 调用addCount()方法计算集合的长度,并判断是否需要扩容。
public V put(K key, V value) {
return putVal(key, value, false);
}
/**
① 当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,如果没有的话就初始化数组
② 计算hash值来确定要添加的数据放在数组的哪个位置,即数组的桶下标,并通过CAS原子操作取出该位置元素
③ 如果该位置为空则直接添加,如果不为空的话,则取出这个节点元素
④ 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,
将原数组复制到新的数组,则当前线程也去帮助复制
⑤ 最后一种情况就是,如果这个节点不为空,也不在扩容,则通过synchronized来加锁,进行添加节点操作
判断当前取出的节点位置存放的是链表还是树:
如果是链表的话,则遍历整个链表,将取出来的节点的key来个要放的key进行比较,
如果key相等,且key的hash值也相等,则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾
如果是树的话,则调用putTreeVal方法把这个元素添加到树中去
⑥ 最后在添加完成之后,会判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话,
则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
//注意:HashMap中允许空键空值,但是ConcurrentHashMap不允许
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); //基于key计算hash值
int binCount = 0; //记录某个桶上元素的个数,如果超过了8个,就会从链表转换为红黑树
for (Node<K,V>[] tab = table;;) { //死循环,除非break,否则一直执行
Node<K,V> f; int n, i, fh;
//① 如果数组还未初始化,先初始化数组
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//② 根据hash值计算数组桶下标,并通过CAS原子操作取出该位置的元素,判断是否为null
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;
}
//③ 如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩容的数据复制阶段
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//④ 当前线程不为null,并且也不在扩容,那么使用Synchronize加锁向桶中该位置添加节点
else {
V oldVal = null;
//加锁,锁对象是数组桶中的头结点
synchronized (f) {
if (tabAt(tab, i) == f) { // 再次取出要存储的位置的元素,跟前面取出来的比较
if (fh >= 0) { // 取出来的元素的hash值大于0,说明为链表;当转换为树之后,hash值为-2
binCount = 1;
//遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 要存的元素的hash,key跟要存储的位置的节点的相同,替换掉该节点的value
if (e.hash == hash && ((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 遍历到链表尾部都没有重复的key,那么将节点添加到链表的尾部
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,value, null);
break;
}
}
}
//如果给节点是树节点
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 在添加完之后,会对该节点上关联的的数目进行判断,
// 如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容(数组长度小于64)
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//计算集合长度,判断是否需要扩容
addCount(1L, binCount);
return null;
}
① 当数组正在扩容时,就会将原数组中的节点从后向前搬迁到新的数组中,搬迁完以后会在该桶中的头节点处添加一个ForwordingNode节点,凡是带有这个节点的代表数组正在扩容,同时当前节点的hash设置为MOVED,此时不能向这个数组中添加元素,当前线程只能协助扩容
② 关于锁对象的说明:我们将数组对应桶处的首节点对象作为锁对象,这样当其他线程想要向该桶中添加元素时,需要先获取锁对象,可以保证在向这个桶中添加元素时是线程安全的。比如对于桶下标为1的桶的首节点为柳岩,那么如果当前线程已经获取了锁对象柳岩节点,并且正在向这个桶中添加元素,其他线程是无法向该桶中添加元素的。
注意:向桶下标为1的桶中添加元素,是不会影响其他线程向其他桶下标为2的桶中添加元素的。这是与HashTable不同的地方,因为HashTable在向数组中添加元素时,整个数组都会锁住,但是ConcurrentHashMap只会锁住要添加元素位置处的那个桶。
static final int spread(int h) {
//通过高16bit 和 低16bit 混合计算出 16bit 的哈希值,充分利用所有信息计算出哈希值,减少hash冲突
return (h ^ (h >>> 16)) & HASH_BITS;
}
/**
* Initializes table, using the size recorded in sizeCtl.
* 初始化数组table,
* 如果sizeCtl小于0,说明别的数组正在进行初始化,则让出执行权
* 如果sizeCtl大于0的话,则初始化一个大小为sizeCtl的数组
* 否则的话初始化一个默认大小(16)的数组
* 然后设置sizeCtl的值为数组长度的3/4,作为下次扩容时的阈值
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 利用CAS+自旋的方式将SIZECTL设置为-1
while ((tab = table) == null || tab.length == 0) {
// 如果sizeCtl小于0,说明别的数组正在进行初始化,则让出CPU执行权
if ((sc = sizeCtl) < 0)
Thread.yield();
// 如果sizeCtl大于0,则初始化一个大小为sizeCtl的数组
// 通过CAS+自旋线程安全的将SIZECTL设置为-1,sc表示期望值,-1表示要替换的值,-1表示要初始化table了
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// double-check方式,双重校验
if ((tab = table) == null || tab.length == 0) {
// sc>0说明已经指定了初始化容量,数组容量初始化为指定大小,否则初始化为默认大小16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// 根据数组容量大小n创建一个数组,完成数组的初始化
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//n-0.25*n = 0.75*n =>初始化完成后,sizeCtl变为下次扩容的阈值
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
//返回初始化后的数组
return tab;
}
//CAS原子操作:返回数组指定位置处的节点
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
// cas原子操作,在指定位置设定值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
https://www.cnblogs.com/zerotomax/p/8687425.html#go6
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}