文章目录
ConcurrentHashMap
JDK 8 ConcurrentHashMap源码分析
ForwardingNode在一个桶数组中的元素全部被搬迁完毕的时候会用来标记那个桶数组,表示它已经搬迁完毕了;同时还有一个作用就是当访问桶数组的时候,如果一个桶数组项已经被ForwardingNode标记了,那么就应该在新的桶数组(已经被扩容了的数组)中进行数据的访问;先扩容再红黑;大于8小于6;
构造器方法
// 初始容量,负载因子,并发度
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
// JDK8的加载方式是懒惰加载的,在构造方法中仅仅是计算了table的大小,在第一次使用的时候才会创建出table本身
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
// tableSizeFor保证最终计算出来的大小是2^n,即16, 32, 64等
// 因为后续的一些hash算法要求哈希表的大小都必须是2^n才能够正常工作
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
get方法
// get方法没有加锁,所以效率是很高的,并发度也很高
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// spread保证计算出的哈希码是正整数
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
// (n - 1) & h就相当于是一个取模运算,效率更高,找到桶数组的下标
// tabAt找到桶数组下标处的头结点
(e = tabAt(tab, (n - 1) & h)) != null) {
// 比较头结点的hash码是不是就是给出的key的hash码
if ((eh = e.hash) == h) {
// hash码是相同的,判断要查找的数和桶数组中头结点的key是不是一样的,如果是的话就直接返回值
// 如果用==判断出二者不是同一个对象的话就判断二者的值是不是相等的
// 也就是说不管二者是同一个对象还是二者的值是相等的都认为是同一个key
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// hash值为负数就表示该bin在扩容中是treebin(-2)
// 这时调用相应的find方法去红黑树中查找目标数据
// 或者头结点是ForwardingNode,ForwardingNode的hash值也是负数(-1)
// 此时去新的桶数组中查找数据
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;
}
put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
// 如果onlyIfAbsent取值为真,就表示只有第一次put键和值的时候才会将它们放入到map
// 之后遇到相同的key的时候并不会用新值覆盖掉旧的值,而是什么都不做
// 是false就表示会用新值覆盖掉旧值
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 普通的HashMap允许存在空的键和值
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
// 代表是链表的长度
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 此时表示哈希表还没有创建
if (tab == null || (n = tab.length) == 0)
// 创建hash表,使用的是CAS操作,只有一个线程会创建成功
// 如果在创建成功之后put操作还没有执行完毕,在下一轮循环会继续执行put
tab = initTable();
// 判断是否有头节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 通过CAS的方式创建头结点,如果创建失败了会再下一次循环中继续尝试将节点放置于map中
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 检查头节点是不是ForwardingNode
else if ((fh = f.hash) == MOVED)
// 锁住当前的链表帮忙扩容
tab = helpTransfer(tab, f);
// 既不是正在扩容,也不是正在初始化table,而是发生桶下标了冲突
else {
V oldVal = null;
// 只有在发生桶下标冲突的时候才加锁,并且加锁的粒度仅仅是链表的头节点
synchronized (f) {
if (tabAt(tab, i) == f) {
// fh >= 0的一定是普通节点,而不是红黑树的根节点或ForwardingNode
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// key已经存在了就执行更新操作
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
// 赋值到旧的value值
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 已经是最后一个节点了,表示key不存在,新增Node追加到链表的结尾
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;
}
}
}
}
// 释放锁之后对链表进行优化,binCount != 0表示链表中存在冲突
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 如果binCount链表长度大小已经超过了树化的阈值就将链表转化为红黑树
// 注意不是立即树化的,而是先将链表进行扩容,如果还是存在binCount大于阈值的情况再进行树化
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 设置多个累加单元进行计数
addCount(1L, binCount);
return null;
}
initTable()方法
// 保证只有一个线程在创建table,其它的线程都是在忙等,并没有阻塞
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 判断table是不是已经创建了
while ((tab = table) == null || tab.length ==