dddd~ 这周我们来拿下ConcurrentHashMap(JDK1.8)
前序
ConcurrentHashMap可以理解为是并发安全的HashMap,在JDK1.7及之前,HashMap在resize()方法中使用头插法进行扩容,这样的操作也给HashMap带来了并发安全的问题,至于为什么会,后续会在出一篇文章解释。在JDK1.8以后,改成了尾插法进行扩容,这样就避免了resize()并发安全问题,但对于HashMap而言,在高并发的情况下还是会存在如:死循环(多个线程同时修改同一个数据,导致数据结构的不一致性,进而在尝试访问或修改数据时陷入无限循环)等,所以在高并发的情况下,强烈推荐大家使用ConcurrentHashMap.
本篇文章,我们涉及的部分有主要参数、整体结构、构造方法、put()方法、initTable()方法
主要参数
/**数组的最大长度*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**默认的初始表容量,一定是2的n次幂*/
private static final int DEFAULT_CAPACITY = 16;
/** 最大可能的(非2的幂)数组大小*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**该表的默认并发级别。未使用但为了与这个类以前的版本兼容而定义的。*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**负载因子 用于计算扩容的阈值*/
private static final float LOAD_FACTOR = 0.75f;
/** 链表转为红黑树存储的阈值*/
static final int TREEIFY_THRESHOLD = 8;
/** 解除红黑树存储的阈值*/
static final int UNTREEIFY_THRESHOLD = 6;
/** 最小转为红黑树的数组长度*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**每个传输步骤的最小重新绑定数。范围被细分以允许多个调整大小线程。此值用作下限,以避免调整大小器遇到过多的内存争用。该值应为“至少” DEFAULT_CAPACITY。*/
private static final int MIN_TRANSFER_STRIDE = 16;
/**在sizeCtl中用于生成戳记的位数。对于32位数组,必须至少为6。*/
private static int RESIZE_STAMP_BITS = 16;
/** 可以帮助调整大小的最大线程数。*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/** 在sizeCtl中记录尺寸戳的位移。*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
/**表初始化和调整大小控件。当为负值时,表正在初始化或调整大小:-1表示初始化,否则-(1 +活动调整大小线程的数量)。否则,当table为null时,将保留创建时使用的初始表大小,或者保留默认值为0。初始化后,保存要调整表大小的下一个元素计数值。*/
private transient volatile int sizeCtl;
整体结构
由图可见数组中存放的是一个一个的节点,volatile 修饰的val保证了val值的可见性(也就是只要这个值发生变化了,都会被刷到主内存中,别的线程的内存对应该数据都会被刷新 ps:这一块可以学习一下JMM)
与HashMap的第一个不同处:对应的节点数组,val值 都是由volatile来进行修饰
构造方法
/**
* Creates a new, empty map with the default initial table size (16).
*/
public ConcurrentHashMap() {
}
//指定容量初始化
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
//容量 = 初始化容量*3/2+1的最近的2次幂的值
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
//这里的tableSizeFor跟HashMap的是一致的
//阈值 = 容量
this.sizeCtl = cap;
}
/**
指定容量和负载因子初始化
*/
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
//指定容量和负载因子初始化和并发级别
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//最小的容量都要满足并发的等级
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
总体来看,跟HashMap的出入不大,都是初始化了数组的阈值。看到这还是会比较模糊的吧~所以我们接下来从put方法入手,解析在构造方法中用到的sizeCtl都在哪用到了
put()
public V put(K key, V value) {
return putVal(key, value, false);
}
同hashMap,都是通过调用名称为putVal()这个方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
//这里,最让大家知道它与HashMap有不同的地方!ps:文章末我们做解释
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();
//如果当前节点数组的位置为空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//尝试通过CAS将该值设置到该索引上
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
//成功则返回
break;
}
//如果判断到当前节点处于MOVED状态(也就是在扩容的状态)
else if ((fh = f.hash) == MOVED)
//那么就帮助扩容并且返回新数组
tab = helpTransfer(tab, f);
else {
//否则,就是到了遍历链表的时候了
V oldVal = null;
//这是jdk1.7和jdk1.8不同的地方之一(1.8是锁当前的数组节点)当前只有一个线程能够操作
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;
//onlyIfAbsent 默认为false
if (!onlyIfAbsent)
//替换值
e.val = value;
break;
}
//如果遍历到链表尾,就进行一个next指针的指向
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;
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;
}
initTable()
在判断当前数组为空的情况,那么就进行数组的初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//当数组为空的时候(第一次检查)
while ((tab = table) == null || tab.length == 0) {
//如果当前的siezCtl<0的时候 (代表有线程在执行扩容)
if ((sc = sizeCtl) < 0)
//释放当前CPU的资源
Thread.yield();
//如果sizeCtl>=0,那么就通过cas将当前的sizeCtl设置为-1,代表当前线程获取初始化的时间片,进行初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//第二次检查,避免并发下,防止其他线程在当前线程等待时已经完成初始化
if ((tab = table) == null || tab.length == 0) {
//初始化长度为 sizeCtl/默认长度16
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); // n*3/4
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
初始化方法在于两个实现关键点:
- sizeCtl值的判断
- 双重检查,避免多次初始化
helpTransfer()
在put方法中,我们看到了有一个神奇的方法,在当前线程在执行put方法的时候,如果当前索引的数组节点被标记为MOVE那么就会执行该方法,那这个方法究竟是做什么的呢?
- 从方法名字看,帮助移植,那我们就能初步的理解为帮忙扩容的?让我们打开源码,一探究竟
forwardingNode<K,V>
首先,我们了解一下这个节点是做什么的
// A node inserted at head of bins during transfer operations.
// 在传输操作期间插入到箱子头部的节点
static final class ForwardingNode<K,V> extends Node<K,V> {
//nextTable指向的是新的数组
final Node<K,V>[] nextTable;
//当原数组中的桶数据进行迁移完毕,那么该桶节点就会被标记为ForwardingNode
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
......
}
从方法中,我们可以看出这个方法就是作为一个判断的辅助节点,用于告知线程当前桶中的所有节点是否都已经扩容转移完成了,也就代表原数组在执行扩容,所以才会有我们下面方法中的判断是否索引节点是否是ForwardingNode。
当前线程表示,我也想插一腿帮你扩容一下
helperTransfer()
/**
*@param tab 当前节点数组
*@param f 当前索引节点
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
//如果当前节点数组不为空,并且当前索引节点是ForwardingNode的实例
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
//当新数组相同并且原数组相同,并且当前线程获取到执行片段
//代表在进行扩容中
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//不满足一下条件则退出
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
//通过cas增加sizeCtl,然后进行扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
//进行实际的扩容
transfer(tab, nextTab);
break;
}
}
//返回新的数组
return nextTab;
}
//否则返回原数组
return table;
}
/**
* Returns the stamp bits for resizing a table of size n.
* Must be negative when shifted left by RESIZE_STAMP_SHIFT.
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
插一腿进来以后就看能不能拿到对应的工具,能拿到对应的工具(cas修改sizeCtl成功)后,你就可以去执行transfer方法了,那就可以开始扩容啦
transfer()
铺垫了那么久,终于 transfer()出来辽。 这个方法,我们分为以下部分进行阐述:
- 计算线程的步长,就是每个线程执行的区间长
/**
* Moves and/or copies the nodes in each bin to new table. See
* above for explanation.
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//计算每个线程的扩容步长,也就是每个线程负责的区间大小(跟服务器的配置有关,根据CPU数量进行分配)
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
//如果新数组为空,进行新数组的初始化,扩容为原来的两倍
if (nextTab == null) {
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
..........
}
根据cpu进行每个线程负责区间长的进行计算,如果没有初始化新数组,扩容为原来的两倍
- 进行对每个线程数组索引区间的分配
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
计算步长和初始化完新数组
....
//新数组长度
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 用于判断当前线程负责的区域是否完成 是否到下一个桶
boolean advance = true;
// 用于判断当前扩容是否已经结束
boolean finishing = false;
// i 是 索引下标 bound代表当前线程处理的上界限
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//当advance为true
while (advance) {
int nextIndex, nextBound;
//当前线程完成设定区间||分配完成
if (--i >= bound || finishing)
advance = false;
//当transferIndex <=0也就是所有线程分配完成 transferIndex是一个全局变量
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//通过CAS修改nextBound值
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//如果能通过CAS设定新的transferindex 并且更新 bound 和i值
//例如步长为4 数组长度为16 那么transferindex在 CAS成功的情况下 分别是
//16 12 8 4 0
bound = nextBound;//12 8 4 0
i = nextIndex - 1;//15 11 7 3 -1
advance = false; //当前线程分配完成
//区间就被分配为[i,bound]
}
}
........
}
- 每个线程对自己负责的区间进行扩容
for (int i = 0, bound = 0;;) {
......
//实际迁移过程
//如果i<0 或者i>=数组长度||i+n>=新数组长度
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//如果resize()(扩容)完成
if (finishing) {
//nextTable置为null 释放内存,帮助GC
nextTable = null;
//新旧数组交替
table = nextTab;
//sizeCtl阈值等于 n*2-n/2;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//通过cas将sc减一
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//如果当前sc-2不等于当前扩容状态
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//否则表示扩容完成
finishing = advance = true;
i = n; // recheck before commit
}
}
//如果当前旧数组节点为空
else if ((f = tabAt(tab, i)) == null)
//通过cas将旧数组节点置为fwd节点,代表该节点已经被处理过
advance = casTabAt(tab, i, null, fwd);
//如果正在执行扩容
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
//同步处理各个桶数据
else {
synchronized (f) {
//tabAt(),通过volatile关键字获取最新的value值
if (tabAt(tab, i) == f) {
//这里同HashMap的扩容,高低位链表
//处理链表数据,
Node<K,V> ln, hn;
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
//确定最后一个高低位节点的位置,将链表拆分成高低位链表,
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
//循环更新ln 和hn 不断将next指针指向新的后一个节点
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
//如果是红黑树,进行红黑树的扩容,从代码来看,跟上面的逻辑基本一致.
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
欢迎大家指出不正确的地方和不明白的地方,我们一起探讨