目录
1.ConcurrentHashMap
1.1整体框架
1.2put方法源码解析
1.3数组初始化时的源码解析
1.4扩容的源码分析
1.5get方法源码解析
static final class HashEntry {
final int hash;
final K key;
volatile V value;
volatile HashEntry next;
...
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//计算hash
int hash = spread(key.hashCode());
int binCount = 0;
//整个的循环
for (Node[] tab = table;;) {
Node f; int n, i, fh;
//table是空的,进行初始化数组
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果当前索引位置没有值,直接CAS创建
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node(hash, key, value, null)))
break;
}
//如果当前槽点是转移节点,表示该节点已经重哈希到新数组上了不能修改,然后就一直等待扩容完成
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//槽点上有值的
else {
V oldVal = null;
//锁定当前槽点,其余线程不能操作,保证了安全
synchronized (f) {
//从赋值到安全点这个过程不安全,需要在判断 i 索引位置的数据没有被修改
//binCount 被赋值的话,说明走到了修改表的过程里面
if (tabAt(tab, i) == f) {
//链表
if (fh >= 0) {
binCount = 1;
for (Node e = f;; ++binCount) {
K ek;
// 有重复值,判断是否进行更新
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node pred = e;
// 没有重复值就会到达链表尾部,直接新增
if ((e = e.next) == null) {
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
//红黑树TreeBin 需要对根节点加锁,槽点不一定是根节点所以需要再次加锁
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
// 如果有重复的值会判断,是否进行更新
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//binCount不为空,并且 oldVal 有值的情况,说明已经新增成功了
if (binCount != 0) {
// 链表是否需要转化成红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
// 新增失败才会走到这里,基本不会走到这里
break;
}
}
}
//check 容器是否需要扩容,如果需要去扩容,调用 transfer 方法
addCount(1L, binCount);
return null;
}
计算哈希值找到对应的槽点,如果数组为空会进行初始化,如果槽点为空(没有哈希冲突)通过CAS进行设置值,如果槽点是转移节点会进行等待扩容完成在进行
如果哈希出现了冲突,首先对槽点进行加锁,然后在判断一下槽点的期望值是否符合当前值,满足后:
如果是链表那么就会进行遍历,如果遇到重复值就会判断是否进行更新,否则就会在尾部新增节点
如果是红黑树那么需要对根节点进行加锁(因为槽点不一定是根节点),也会进行重复值的更新是否进行判断
最后,判断是否需要链表转化为红黑树,如果有只是更新了值那么直接返回,最终判断是否需要进行扩容。
//初始化 table,通过对 sizeCtl 的变量赋值来保证数组只能被初始化一次
private final Node[] initTable() {
Node[] tab; int sc;
//通过自旋保证初始化成功
while ((tab = table) == null || tab.length == 0) {
// 小于 0 代表有线程正在初始化,需要释放当前 CPU 的调度权
if ((sc = sizeCtl) < 0)
Thread.yield();
// CAS 原子操作通过变量赋值,让线程可以阻塞在上面的if判断
// 但是,可能线程1已经运行过上面的if了,运行在这里然后切换了
// 直到线程2运行完下面的所有操作,线程1开始恢复进入了CAS操作
// 所以需要再次进行判断
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 很有可能执行到这里的时候,table 已经不为空了,这里是双重 check
if ((tab = table) == null || tab.length == 0) {
// 进行初始化
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node[] nt = (Node[])new Node[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
初始化操作没有通过锁来保证安全性,而是通过复杂的CAS设置变量,来进行阻塞,并且加入双重判断来保证安全性,
ps:第二个判断,主要还是因为可能有多个线程执行完了第一个if,都进入到了CAS操作
//addCount方法的部分,保证了初始化的正确性
int rs = resizeStamp(n);
if (sc < 0) {
if ((nt = nextTable) == null || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc - 1))
transfer(tab, nt);
太长太复杂了,简单说下思路吧!
首先创建数组(新数组为老数组大小的两倍),也是通过CAS设置变量值+双重判断来进行初始化的
然后从后向前拷贝旧数组的槽点,拷贝的时候会先把槽点锁住(链表就不需要再加锁了和红黑树还需要在根节点加锁),拷贝完成后会设置为转移节点,就不能在进行更改了直到扩容完成。
public V get(Object key) {
Node[] tab; Node e, p; int n, eh; K ek;
//计算hashcode
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;
}
// 如果是红黑树或者转移节点,使用对应的find方法
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;
}
首先计算出索引值,判断hashcode值在判断key是否相等,如果相等直接返回,否则一定出现了哈希冲突
链表进行查找就是从前向后进行遍历,
红黑树查找就是二叉排序树进行查找,通过比较大小判断是在左子树还是在右子树上,