# ConcurrentHashMap
HashMap虽然好用,但是它却不是线程安全的,而在并发度较高的现在,在有些情况下它可能就不是那么合适了,所以需要一个线程安全键值对结构。
Hashtable是线程安全的,但是它却过于笨重了,相比于HashMap而言,它仿佛只是在HashMap的每一个方法上加了一个synchronized,效率可想而知。所以就有了我们今天这篇问文章的主角ConcurrentHashMap,毕竟讲HashMap不讲ConcurrentHashMap那就是耍流氓,想要了解HashMap的同学可以去[HashMap源码及其常见问题详解](https://blog.csdn.net/weixin_42737868/article/details/113256318)看看。
注意,我们这里讲解的ConcurrentHashMap同样是Java8的。
## 关键属性
ConcurrentHashMap为什么能保证线程安全的呢,简单来说是**通过CAS+synchronized来实现的**,而且他最大的特点是能够多个线程协助扩容,至于是怎么实现的且听我慢慢道来。
```java
private transient volatile int sizeCtl;
```
ConcurrentHashMap相比于HashMap多了一个关键的属性`sizeCtl`,它有这几种情况:
1. -1:代表正在初始化;
2. -N:代表有N-1个线程正在协助扩容;
3. 0或N:当table为null的时候代表初始容量,0代表默认容量,否则代表扩容阈值;
可以看出`sizeCtl`身兼数职,它不但代替了HashMap的`threshold`属性,还在数组初始化和扩容的时候有着至关重要的作用。我们先对他有个大致的了解,具体的应用我们下文再说。
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
}
另一个巨大的改变是多了一个特殊的节点`ForwardingNode`,他只有在扩容的时候用的到,ConcurrentHashMap**在扩容的时候将这个节点插入桶的头部,来告诉其他线程这个桶正在被迁移。**`nextTable`是扩容的时候的**新数组**。`ForwardingNode`节点的**key和value都是null**,所以ConcurrentHashMap**不允许节点的key或value为空**。`ForwardingNode`节点的hash值默认是MOVEN,也就是-1,同样我们可以看到红黑树根节点的hash值是-2。
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
他跟HashMap的另一个属性的不同是多了一个`nextTable`,他只在扩容的时候有用,其他时候都是null。
/**
* The next table to use; non-null only while resizing.
*/
private transient volatile Node<K,V>[] nextTable;
## put()
ConcurrentHashMap的构造函数跟HashMap大差不差,都是对一些关键属性的赋值,如果没有指定就全都是默认,所以我们这里就不细讲了,有兴趣的可以看我上一篇文章,当然我也推荐你看完上一篇文章之后再来学习这一篇文章。
`put()`可以说是HashMap最核心的方法了,把这个方法完全搞懂基本上就把HashMap全部学会了,所以我们这次依旧来深入`put()`方法。
//key value都不能为null
public V put(K key, V value) {
return putVal(key, value, false);
}
`put()`还是一样,实际逻辑都在`putVal()`里。
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<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//还未初始化的时候先进行初始化
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<K,V>(hash, key, value, null)))
break; // 添加的时候没有加锁
}
//hsah值是-1的是ForwardingNode节点,说明正在扩容
else if ((fh = f.hash) == MOVED) //static final int MOVED = -1;
//协助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//存在链表且没在扩容,就将头结点锁住
synchronized (f) {
//再次效验节点有没有发生改变
if (tabAt(tab, i) == f) {
//hash值>=0代表是链表
if (fh >= 0) {
binCount = 1;
//遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
//存在这个key就覆盖(如果可以覆盖的话)
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;
}
}
}
//对红黑树的处理
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;