前言
1.8后的ConcurrentHashMap与之前有截然不同的设计,之前是分段锁的思想,通过采用分段锁Segment减少热点域来提高并发效率。1.8利用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。
在此再一次膜拜Doug Lea大神,高山仰止。1.8的ConcurrentHashMap有6313行代码,之前大概是1000多行。
这篇文章也只是概括了部分功能。
本文多介绍的是并发部分,对于底层的散列表操作,红黑树操作,这在之前分析HashMap的文章时已有详细介绍,ConcurrentHashMap关于这些是与HashMap相通的。
这里先来说一下CAS+volatile的组合,这两个是整个JUC包的基石。volatile 读的内存语义:当读一个volatile变量时,JMM会把线程u敌营的本地内存置为无效,线程接下来将从主内存中读取值。volatile写的内存语义:当写一个volatile变量时,JMM会到主内存中取读值。JSR-133增强volatile内存语义:之前旧的JMM允许volatile变量与普通变量重排序,之后严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile写-读与锁的释放-获取有相同语义。也就是说:写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。A写一个volatile变量,随后B读到这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。而CAS同时具有volatile读/写的内存语义,以Intel X86来说,就是利用在CMPXCHG指令前添加lock前缀来实现。
volatile的读写和CAS可以实现线程之间的通信,整合到一起就实现了Concurrent包得以实现的基石。在阅读JUC下的类时会发现一个通用的模式:volatile的共享变量,CAS原子更新实现线程同步,二者搭配来实现线程之间的通信。很多操作都是先读volatile变量此时的最新值,赋给局部变量,然后一顿操作,最后CAS进行同步,若过程中共享变量被其它线程更改则会导致CAS失败,重新尝试。
相关概念
table
所有数据都被存放在table数组中,大小是2的整数次幂,存储的元素分为三种类型
- TreeBin 用于包装红黑树结构的结点类型 ,它继承了Node,代表它也是个节点,hash值为-2
- ForwardingNode 扩容时存放的结点类型,并发扩容的实现关键之一 ,是一个标记,代表此处已完成扩容,hash值为-1。
- Node 普通结点类型
Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
......
}
value和next都用volatile修饰,保证并发的可见性
ForwardingNode
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作用在扩容期间,hash值为MOVED 值为-1,当table[ i ] 是个ForwardingNode节点时,代表该位置节点已经移至新数组。
nextTable
扩容时新生成的数组,其大小为原数组的两倍
sizeCtl
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
private transient volatile int sizeCtl;
用于table数组初始化及扩容控制,下面来看看它是如何控制的:
接下来的思路是沿着sizeCtl来跟踪源码,但是很多方法具有多种功能,比如addCount...会牵扯很多其它的概念,这里就把他们先剔除出去,先沿着一条简单的线来解读
sizeCtl在初始化与扩容中的作用
1,初始化:ConcurrentHashMap有五个构造器,不考虑构造时指定集合的,其他四个都没有在初始化期间创建table数组对象,而是将这一操作下放到第一次调用put插入键值对时。sizeCtl决定了table数组的大小,无参构造器则sizeCtl为默认值0,若传入了初始值大小,经过tableSizeFor将sizeCtl改为比传入值大的最小2的n次幂,如传15返回16,17返回32。
final V putVal(K key, V value, boolean onlyIfAbsent) {
.......省略
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
......省略
initTable会被调用:这里利用 循环CAS 确保只有一个线程能够初始化table数组
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -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);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
读取voaltile的sizeCtl 值赋给局部变量 sc = sizeCtl,之后当一个线程cas设置sizeCtl值为-1成功,之后的线程都将被拒绝,通过执行Thread.yield()。也就是说只允许一个线程执行初始化table 数组操作。sc == 0则table大小为16,否则就为sizeCtl大小的值。数组创建完成后sizeCtl = n - (n >>> 2),相当于原先值的0.75,这之后sizeCtl代表阀值。
2,接下来看看它在扩容上的控制: 扩容-是transfer方法
先从put入手
put
public V put(K key, V value) {
return putVal(key, value, false);
}
// onlyIfAbsent为true:相当于putIfAbsent,即key存在就不更换value
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key与value都不能为null
if (key == null || value == null) throw new NullPointerException();
//(h ^ (h >>> 16)) & HASH_BITS;//0x7fffffff;保证了hash >= 0.
int hash = spread(key.hashCode()); //计算出hash
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 初始化table数组操作,若sizeCtl为0则table大小为16,否则为sizeCtl大小
tab = initTable();
//通过hash得到数组下标,若该位置为null,新建节点放在该位置上
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//MOVED为-1,代表该位置的头节点为forwarding nodes,表明该位置正在进行扩容
//helpTransfer,让该线程帮助进行扩容操作。
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 (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,