目录
为什么用ConcurrentHashMap而不用HashMap或者HashTable?
为什么用ConcurrentHashMap而不用HashMap或者HashTable?
在涉及多线程的时候用ConcurrentHashMap或者HashTable,HashMap不是线程安全的。拿put方法来说HashTable是在整个方法上加锁,而ConcurrentHashMap只在发生冲突时才加锁,这样很大程度上减少了阻塞。
HashTable中的put方法如下:
public synchronized V put(K key, V value)
哈希算法的两大关键
为什么说这个呢?因为ConcurrentHashMap的put方法,实际上就是解决哈希冲突的过程,所以了解下基本概念还是有必要的。两大关键:
- 散列函数
- 处理冲突的方法
常见处理冲突的方法:
- 开放定址法:
根据d的不同取值又可以分为线性探测法、平法探测法和双散列法。
- 拉链法
ConcurrentHashMap中的put方法
全部代码如下:
public V put(K key, V value) {
return putVal(key, value, false);
}
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;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
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);
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;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
在put方法中调用了putVal方法,主要逻辑都在这里面。
按行来看代码:
if (key == null || value == null) throw new NullPointerException();
ConcurrentHashMap要求key和value都不能为空,否则抛出异常。
int hash = spread(key.hashCode());
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
计算哈希值。拿到key的哈希值后,对低16位和高16位进行混合,减少发生碰撞的可能性。比如key.hashCode()为 01010101 01010101 10101010 10101010,无符号右移16位后变成了 00000000 00000000 01010101 01010101,再进行异或后变成了 01010101 01010101 11111111 11111111。HASH_BITS的值为0x7fffffff,即最高位是0,其余位是1。与HASH_BITS想与相当于把最高位变成0,其余位保持不变。
for (Node<K,V>[] tab = table;;) {
进入无限循环,在循环里面插入成功或失败后会返回(即退出循环)。table是ConcurrentHashMap中保存数据的结构。
if (tab == null || (n = tab.length) == 0)
tab = initTable();
进入无限循环,在循环里面插入成功或失败后会返回(即退出循环)。table是ConcurrentHashMap中保存数据的结构。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
(n - 1) & hash把哈希值转换为数组下标(或者说把哈希值限定在数组范围之内)。tabAt(tab, i = (n - 1) & hash)找出当前位置的Node值,如果是null,那就说明没有发生冲突,可以进行插入操作。casTabAt(tab, i, null, new Node<K,V>(hash, key, value))cas算法插入新的结点。cas算法原理:获取当前内存的位置,还有一个期望值,如果相等,表示没有其他线程进行修改,可以插入,不然不做任何操作。如果发生了冲突,那么接着往下走。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
MOVED是一个特殊的标记,表示正在进行扩容。
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
onlyIfAbsent 表示只进行检查不进行插入操作,直接返回当前冲突结点的value。
V oldVal = null;
synchronized (f) {
首先定义一个oldVal,因为最终要返回发生冲突的节点的旧值。然后终于加上了锁。
binCount = 1;
binCount表示发生冲突的次数。
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
如果插入结点的key和hash值与当前结点的key和hash值一致,那么更新当前结点的value。
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break;
}
e = e.next用线性探测法探测下一个结点。
else if (f instanceof TreeBin)
如果是红黑树,那么插入红黑树。
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
如果冲突次数超过了指定的值,那么把链表转化为红黑树。
addCount(1L, binCount);
容量加1。
简单总结就是:没有冲突,则直接插入,返回null;发生了冲突,返回key一致,那么更新为新的value,返回旧value,如果key不一致,探测下一个位置,再重复循环上面这些步骤。