秋招复习笔记系列目录(不断更新中):
- 1.数据结构全系列
- 2.计算机网络知识整理(一)
- 3.计算机网络知识整理(二)
- 4. Java虚拟机知识整理
- 5.计算机操作系统
- 6.深入理解HashMap
- 7.深入理解ConcurrentHashMap
- 8.MySQL
一、前言
以前感觉HashMap难懂,直到我看了ConcurrentHashMap。。。不过,等真的读懂了源码,不得不感叹,Doug Lea大爷还是你大爷,看的过程中,不时惊呼:原来是这样啊!这也太牛了!好了,首先介绍一下,ConcurrentHashMap是一个线程安全的HashMap,其主要采用CAS操作+synchronized锁的方式,实现线程安全,其中synchronize锁的粒度为桶中头结点(包括链表Node结点,包装红黑树的TreeBin结点),底层依然由 “数组”+链表+红黑树 的方式实现。
二、基础
2.1 几个重要的属性
-
sizeCtl: 它是一个控制标志符,取值不同有不同的含义:
- 负数代表正在进行初始化或扩容操作
- -1代表正在初始化
- -N 表示有N-1个线程正在进行扩容操作
- 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,这一点类似于扩容阈值的概念。它的值始终是当前
ConcurrentHashMap
容量的0.75倍,这与loadfactor
是对应的。
-
桶中元素的hash值的含义:
- ①hash==-1 (MOVED) : 表示当前节点是ForwardingNode节点
- ②hash==-2 (TREEBIN) : 表示当前节点已经树化,且当前节点为TreeBin对象,TreeBin节点代理红黑树操作
- ③hash==-3 (RESERVED) : 临时保留的哈希
2.2 几个重要的内部类
- Node节点类: 和
HashMap
中的节点类似,只是其val
变量和next
指针都用volatile
来修饰。且不允许调用setValue
方法修改Node的value
值。这个类是后面三个类的基类。 - TreeNode: 树节点类,当链表长度过长的时候,会转换为
TreeNode
。但是与HashMap
不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode
放在TreeBin
对象中,由TreeBin
完成对红黑树的包装。而且TreeNode
在ConcurrentHashMap
继承自Node类,而并非HashMap
中的继承自LinkedHashMap.Entry<K,V>
类,也就是说TreeNode
带有next指针,这样做的目的是方便基于TreeBin
的访问。 - TreeBin: 这个类并不负责包装用户的key、value信息,而是直接放了一个名为
root
的TreeNode
节点,这个是他所包装的红黑树的根节点,也就是说在实际的ConcurrentHashMap
“数组”中,存放的是TreeBin
对象,而不是TreeNode
对象,这是与HashMap
的区别。另外这个类还带有了读写锁。HashMap桶中存储的是TreeNode
结点,这里的根本原因是==并发过程中,有可能因为红黑树的调整,树的形状会发生变化,这样的话,桶中的第一个元素就变了,而使用TreeBin
包装的话,就不会出现这种情况。 这种类型的节点, hash值为-2,从下面的构造函数中就可以看出来。这样我们通过hash值是否等于-2就可以判断桶中的节点是否是红黑树。
- ForwardingNode: 一个用于连接两个table的节点类。它包含一个
nextTable
指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable
里进行查询节点,而不是以自身为头节点进行查找。在扩容操作中,我们需要对每个桶中的结点进行分离和转移,如果某个桶结点中所有节点都已经迁移完成了(已经被转移到新表 nextTable 中了),那么会在原 table 表的该位置挂上一个 ForwardingNode 结点,说明此桶已经完成迁移。这种类型的节点的hash值是-1,通过构造函数也可以看出来
2.3 使用CAS操作的三个核心方法
类中定义了三个静态的用于CAS操作的方法:
//获得在i位置上的Node节点
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
//利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少
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);
}
//利用volatile方法设置节点位置的值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
三、数组的初始化
数组的初始化是在ConcurrentHashMap
插入元素的时候发生的,如调用put
等方法时发生的。初始化操作在initTable
方法中,该没有加锁,因为采取的策略是,当sizeCtl<0
时,说明已经有线程在给扩容了,这个线程就会调用Thread.yield()
让出一次CPU执行时间。看代码,我们可以看到上面说的sizeCtl
的作用:负数表示正在扩容,扩容完成后,用来表示阈值。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//tab为空时才进行初始化
while ((tab = table) == null || tab.length == 0) {
//如果sizeCtl<0,说明有其他的线程正在初始化,当前线程让出资源
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//当前没有线程扩容,那就利用CAS方法把sizectl的值置为-1 表示本线程正在进行初始化
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//开辟数组作为桶
table = tab = nt;
sc = n - (n >>> 2);//相当于0.75*n 用sizeCtl来表示阈值
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
四、put过程
4.1 整体过程
整个put过程,主要在putVal
函数中实现,具体过程为:
- ① 如果数组未没初始化,则先去初始化
- ② 如果对应的桶中的元素为空,那就新建一个链表节点,然后利用CAS操作将其放到桶中的位置。这个过程是在③前面的,我们知道,扩容过程中,每个桶位置迁移节点结束后,会将这个节点设置为
ForwardingNode
,所以这种情况下,你尽管放,放了以后,扩容的线程总会遍历到这个节点,然后将这个节点迁移到新数组中。 - ③ 如果有线程在扩容,那就先去帮助扩容,扩容结束后,再重新put。
- ④ 最后,如果当前桶中已经有元素了,那就用synchronized锁住当前桶中的节点,然后在桶中插入元素,插入的时候,要么插入到链表中,要么插入到红黑树中。我们发现,这里的锁粒度是很小的,就锁住一个桶,不像JDK1.7中的
ConcurrentHashMap
,是分段锁,锁住很多的桶,所以并发效率更高。 - ⑤ 插入结束后,如果是插入到链表中,那去看看链表的长度有没有超过长度阈值8,如果超过了,就要将链表转换成红黑树。
- ⑥ 最后,让HashMap的size加一(这里其实是用baseCount来记录长度的,而且处理的时候很复杂,继续看下面)。
整个putVal
函数的代码如下,我对重要的地方都做了注释,应该很容易看懂:
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 (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
ConcurrentHashMap.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 ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果有线程正在扩容,则两个线程一起帮忙扩容,扩容完毕后tab指向新table
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
//持有桶中的第一个节点的锁
if (tabAt(tab, i) == f) {
//加锁后再去判断,桶中的结点有没有变化
if (fh >= 0) {
//表明是链表结点类型
binCount = 1;
for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&//找到对象,将其val替换
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
ConcurrentHashMap.Node<K,V> pred = e;
if ((e = e.next) == null) {
//没找到,插入到尾部
pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
value, null);
break;
}
}
}
//红黑树节点的插入
else if (f instanceof ConcurrentHashMap.TreeBin) {
ConcurrentHashMap.Node<K,V> p;
binCount = 2;
if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//binCount不为0说明插入了新节点,为0说明在空桶中插入了一个节点(这种情况不需要树化)
if (binCount != 0) {
//默认桶中结点数超过8个数据结构会转为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}