【0】ConcurrentHashMap 整体架构
构成说明
采用transient volatile HashEntry
采用table数组+单向链表+红黑树的结构
ConcurrentHashMap 和 HashMap 比较
相同之处
1. 数组、链表结构几乎相同,所以底层对数据结构的操作思路是相同的
2. 都实现了 Map 接口,继承了 AbstractMap 抽象类,所以大多数的方法也都是相同的,
HashMap 有的方法,ConcurrentHashMap 几乎都有,
当需要从 HashMap 切换到 ConcurrentHashMap 时,无需关心两者之间的兼容问题
不同之处
1. 红黑树结构略有不同,
HashMap 的红黑树中的节点叫做 TreeNode,TreeNode 不仅仅有属性,还维护着红黑树的结构,比如说查找,新增等等;
ConcurrentHashMap 中红黑树被拆分成两块,TreeNode 仅仅维护的属性和查找功能,
新增了 TreeBin,来维护红黑树结构,并负责根节点的加锁和解锁
2. 新增 ForwardingNode (转移)节点,扩容的时候会使用到,通过使用该节点,来保证扩容时的线程安全
【1】ConcurrentHashMap 新增数据
/**
* 单纯的额调用putVal方法,并且putVal的第三个参数设置为false
* 当设置为false的时候表示这个value一定会设置
* 当设置为true的时候,只有当这个key的value为空的时候才会设置
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
//1:如果数组为空,初始化,
//2:计算当前槽点有没有值,没有值的话,cas 创建,失败继续自旋
//3:如果槽点是转发节点(正在扩容),就会一直自旋等待扩容完成之后再新增
//4:槽点有值的,先锁定当前槽点,其余槽点不能操作
//4.1 : 如果是链表,新增值到链表的尾部
//4.2 : 如果是红黑树,在红黑树着色旋转的时候,会考虑把红黑树的根节点锁住
//5:check需不需要扩容,需要的话去扩容.
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key value 不能为空
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)
// 处理初始化,并发的情况在initTable中处理,这里不考虑
tab = initTable();
//如果当前索引位置没有值,直接创建
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//cas 在 i 位置创建新的元素,当i位置是空时,创建成功结束for自循环,否则继续自旋
if (casTabAt(tab, i, null,
new Node(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//发现转发节点,表明此时正在进行扩容,去帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//槽点上有值的
else {
V oldVal = null;
//锁定当前槽点,其余线程不能操作,保证了安全
synchronized (f) {
//这里再次判断 i 索引位置的数据没有被修改
//binCount 被赋值的话,说明走到了修改表的过程里面
//保证锁住的是hash桶的第一个节点,这样阻止其他写操作进入,
//如果锁住的不是第一个节点,那么重新开始循环
if (tabAt(tab, i) == f) {
//链表
if (fh >= 0) {
//因为第一个节点处理了,这里赋值为1
binCount = 1;
for (Node e = f;; ++binCount) {
K ek;
//找到“相等”的节点,看看是否需要更新value的值
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;
}
}
}
//红黑树,这里没有使用 TreeNode,使用的是 TreeBin,TreeNode 只是红黑树的一个节点
//TreeBin 持有红黑树的引用,并且会对其加锁,保证其操作的线程安全
else if (f instanceof TreeBin) {
Node p;
//设置为2,保证addCount中能够进行扩容判断,
//同时也不会触发链表转化为红黑树的操作
binCount = 2;
//满足if的话,把老的值给oldVal
//在putTreeVal方法里面,在给红黑树重新着色旋转的时候
//会锁住红黑树的根节点
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 方法去扩容
//如果已经在扩容中了,check完成
addCount(1L, binCount);
return null;
}
【2】ConcurrentHashMap 查找数据流程
public V get(Object key) {
Node[] tab; Node e, p; int n, eh; K ek;
//计算hashcode
int h = spread(key.hashCode());
//不是空的数组 && 并且当前索引的槽点数据不是空的
//否则该key对应的值不存在,返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//槽点第一个值和key相等,直接返回
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
// hash桶的第一个节点的hash值小于0,代表它是特殊节点,使用特化的查找方式进行查找
// ForwardingNode会把find转发到nextTable上再去执行一次
// TreeBin则根据自身读写锁情况,判断是用红黑树方式查找,还是用链表方式查找
// ReservationNode本身只是为了synchronized有加锁对象而创建的空的占位节点,
// 因此本身hash桶是没节点的,一定找不到,直接返回null
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;
}
【3】ConcurrentHashMap 删除数据流程
public V remove(Object key) {
return replaceNode(key, null, null);
}
// remove删除,可以看成是用null替代原来的节点
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (Node[] tab = table;;) {
Node f; int n, i, fh;
// 没有节点,删除不了,直接退出
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
// 发现转发节点,表明此时正在进行扩容,去帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
//这里跟put一样,尝试锁住hash桶的第一个结点,要保证锁住的是第一个结点
synchronized (f) {
//确保锁住的是第一个节点
if (tabAt(tab, i) == f) {
//处理链表相关的删除/替换操作
if (fh >= 0) {
validated = true;
for (Node e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
//确定了待删除的值
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
//value != null 表明执行的是替换操作
if (value != null)
e.val = value;
//进行删除节点 e 的操作
else if (pred != null)
pred.next = e.next;
//删除的是第一个节点,就重设第一个节点,此时相当于已经释放了锁
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
//判断循环遍历是否已经结束
break;
}
}
//处理红黑树相关的删除/替换操作
else if (f instanceof TreeBin) {
validated = true;
TreeBin t = (TreeBin)f;
TreeNode r, p;
//在红黑树中确定待删除/替换的节点
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
//value != null 表明进行替换操作
if (value != null)
p.val = value;
//红黑树删除节点
//removeTreeNode : 红黑树的规模太小时,返回true
else if (t.removeTreeNode(p))
//处理红黑树退化为链表的情况
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
// 下面这一段判断是否是删除操作,是删除操作就把计数值减1
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
致谢
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。
【1】Java中Unsafe类详解
【2】ConcurrentHashMap的tabAt为什么不用tab[i]
【3】ConcurrentHashMap实现原理及源码分析
【4】Java集合类框架学习 5.3—— ConcurrentHashMap(JDK1.8)