ConcurrentHashMap是线程安全且高效率的HashMap,本文我们将研究一下该容器的具体实现。
目录
为什么要使用ConcurrentHashMap
ConcurrentHashMap实现
JDK1.5中
ConcurrentHashMap的数据结构如图
get方法
put方法
JDK1.8中
ConcurrentHashMap的数据结构如图
put方法
get方法
JDK1.8中的ConcurrentHashMap已经抛弃了分段锁,使用了CAS+synchronized来保证线程安全,所以我们分两部分讲解。
理解ConcurrentHashMap首先要对于HashMap有所了解,如果没有了解的同学可以先看一下我的关于HashMap的博文
【源码分析】深入理解HashMap 学习手记
ConcurrentHashMap中,segment继承了ReentrankLock充当锁的角色,每个segment守护了若干个桶(Bucket)。
在HashMap中,除去segment部分,就是HashMap的数据结构。
我们可以理解为ConcurrentHashMap就是将一个HashMap分成了多个HashMap,并且对每一个HashMap使用继承了ReentrankLock的segment来维护,实现线程安全。
public V get(Object key){
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}
先经过一次散列运算,定位到segment,然后再通过散列运算定位到其中的元素。非常简洁高效。
由于put方法中需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。
put方法首先定位到segment,然后在segment里进行put操作。
插入操作首先需要判断是否需要对segment里的HashEntry进行扩容(在HashMap中插入操作也需要检查是否需要扩容),如果需要扩容则扩容后再插入,否则直接插入。
由于已经弃用,我们不做更细致的讨论,主要看JDK1.8中的ConcurrentHashMap实现。
ConcurrentHashMap在1.8中的实现,相比于1.7的版本基本上全部都变掉了。首先,取消了Segment分段锁的数据结构,取而代之的是数组+链表(红黑树)的结构。而对于锁的粒度,调整为对每个数组元素加锁(Node)。然后是定位节点的hash算法被简化了,这样带来的弊端是Hash冲突会加剧。因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。这样一来,查询的时间复杂度就会由原先的O(n)变为O(logN)。
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());//计算hash值,两次hash操作
int binCount = 0;
for (Node[] tab = table;;) {//类似于while(true),死循环,直到插入成功
Node f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)//检查是否初始化了,如果没有,则初始化
tab = initTable();
/*
i=(n-1)&hash 等价于i=hash%n(前提是n为2的幂次方).即取出table中位置的节点用f表示。
有如下两种情况:
1、如果table[i]==null(即该位置的节点为空,没有发生碰撞),则利用CAS操作直接存储在该位置,
如果CAS操作成功则退出死循环。
2、如果table[i]!=null(即该位置已经有其它节点,发生碰撞)
*/
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
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)//检查table[i]的节点的hash是否等于MOVED,如果等于,则检测到正在扩容,则帮助其扩容
tab = helpTransfer(tab, f);//帮助其扩容
else {//运行到这里,说明table[i]的节点的hash值不等于MOVED。
V oldVal = null;
synchronized (f) {//锁定,(hash值相同的链表的头节点)
if (tabAt(tab, i) == f) {//避免多线程,需要重新检查
if (fh >= 0) {//链表节点
binCount = 1;
/*
下面的代码就是先查找链表中是否出现了此key,如果出现,则更新value,并跳出循环,
否则将节点加入到链表末尾并跳出循环
*/
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)//仅putIfAbsent()方法中onlyIfAbsent为true
e.val = value;//putIfAbsent()包含key则返回get,否则put并返回
break;
}
Node pred = e;
if ((e = e.next) == null) {//插入到链表末尾并跳出循环
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
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;
}
}
}
}
//插入成功后,如果插入的是链表节点,则要判断下该桶位是否要转化为树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)//实则是>8,执行else,说明该桶位本就有Node
treeifyBin(tab, i);//若length<64,直接tryPresize,两倍table.length;不转树
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
从上面代码可以看出,put的步骤大致如下:
检查 key/value
是否为空,处理 hash
值
进入 for
死循环,因为 CAS
的无锁操作需要一直尝试直至成功
检查 table
是否初始化,没有则初始化 initTable()
根据 key
的 hash
值找到在 table
中的位置 i
,取出 table[i]
的节点 f
如果 f==null
(即该位置的节点为空,没有发生碰撞)
直接 CAS
存储,退出循环
如果 f!=null
(即该位置已经有其它节点,发生碰撞),检查 f
的节点的 hash
是否等于 MOVED
a.如果等于,则检测到正在扩容,则帮助其扩容
b.如果不等于,如果f是链表节点,则直接插入链表;如果是树节点,则插入树中
判断 f
是否需要将链表转换为平衡树
并发控制:
使用 CAS
操作插入数据
在每个链表的头结点都使用 Synchronized
上锁
除了上述步骤以外,还有一点我们留意到的是,代码中加锁片段用的是synchronized关键字,而不是像1.7中的ReentrantLock。这一点也说明了,synchronized在新版本的JDK中优化的程度和ReentrantLock差不多了。
public V get(Object key) {
Node[] tab; Node e, p; int n, eh; K ek;
int h = spread(key.hashCode());// 定位到table[]中的i
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {// 若table[i]存在
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;// 未找到
}
get()方法的流程相对简单一点,从上面代码可以看出以下步骤:
从上面步骤可以看出,ConcurrentHashMap的get操作上面并没有加锁。所以在多线程操作的过程中,并不能完全的保证一致性。这里和1.7当中类似,是弱一致性的体现。
JDK1.8中concurrentHashMap的介绍摘自https://blog.csdn.net/fouy_yun/article/details/77816587
参考资料:《Java并发编程的艺术》