简单介绍
这里我研究的是1.8,1.7还没看😀,ConcurrentHashMap的数据结构如下
ConcurrentHashMap的数据结构和HashMap差不多,都是数组+链表+红黑树**的结构。通过拉链法来解决hash冲突,对于链表,如果长度大于8就会判断是否要转换为红黑树。
那么concurrentHashMap如何保证线程安全呢?这里先直接给答案:通过一些原子操作+给bucket加锁来保证线程安全,这样锁的颗粒度就比较小,不用把整个table都给锁了。至少目前我看到的源码是这样。下面放出concurrentHashMap的put方法的源码,我加了注释
源码
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
// onlyIfAbsent的意思这里也解释一下,
//该值如果是false的话,put一个map中已经含有的key,会把该key的value给更新。
//如果为true,put一个map中已含有的key,就不做操作
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
// tab赋值为table
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 1. tab为空,初始化,然后重新进入循环插入node
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 2. 如果当前桶为空,不加锁,通过cas原子操作尝试添加,成功退出循环,失败重新插入node
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 该方法是通过调用cas进行操作的
//会先比较桶的位置的值是否为null,如果为null则进行就该,添加node。如果不为null,其他线程在这个位置中添加了数据,操作失败,重新进入循环
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 3. 这里说明该桶在扩容,获取所在table,然后重新进入循环
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 4. 这里说明,该桶就在该位置,且不为空,则给bucket加锁并插入node
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
// 如果bucket中第一个node的hash值是大于0,说明是一个正常节点,直接遍历到最后或遍历到hash值相等且key相等的位置,并插入
// 对于bucket取特殊值,有不同含义,如下
//MOVED = -1; // hash for forwarding nodes
//TREEBIN = -2; // hash for roots of trees
//RESERVED = -3; // hash for transient reservations
if (fh >= 0) {
binCount = 1;
for (Node<K,V> 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<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 如果该bucket已经是红黑树
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;
}
}
}
}
// 判断是否需要转换成红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
解析
put一个key-value到concurrentHashMap,会遇到下面几种情况:
table没有初始化,进行初始化并重新添加node
上面有一个for的循环,如果没break,会再次进入循环。
- 添加的位置,
bucket为空,不加锁,通过cas原子操作添加
casTabAt方法调用的是cas原子操作,比较并交换。如果该bucket的值依然为null,就添加节点,并且break,退出循环。如果有其他线程修改了这个bucket,那么该位置的值就不为null,操作失败,重新添加
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);
}
- 该bucket在扩容
concurrentHashMap是动态扩容的,呃,这个晚点看
- 其余情况,给
bucket加锁,也就是链表的第一个node,并向里面添加要添加的node
synchronized (f) {
if (tabAt(tab, i) == f) {
// 如果bucket中第一个node的hash值是大于0,说明是一个正常节点,直接遍历到最后或遍历到hash值相等且key相等的位置,并插入
// 对于bucket取特殊值,有不同含义,如下
//MOVED = -1; // hash for forwarding nodes
//TREEBIN = -2; // hash for roots of trees
//RESERVED = -3; // hash for transient reservations
if (fh >= 0) {
...
}
// bucket是红黑树,插入红黑树
else if (f instanceof TreeBin) {
...
}
}
}
所以,在通过put方法可以大致了解到concurrentHashMap是通过一些原子操作和给bucket加锁来保证线程安全的。至少在put方法中是这样😀