ConcurrentHashMap
ConcurentHashMap大致构造与HashMap一样,都是数组 + 链表或者红黑树。但是HashMap是线程不安全的,ConcurrentHashMap是线程安全的。
现在我们来进一步了解它的工作原理。
成员
我们来了解几个成员。
// 这是map最大的容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 如果初始化map的时候没有给定容量大小,那么map的默认容量为16
private static final int DEFAULT_CAPACITY = 16;
// 如果链表的长度等于8,那么就会将链表转换成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 如果树的节点个数等于6,那么树会转换为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 链表转换为树必须满足数组的长度大于等于64
static final int MIN_TREEIFY_CAPACITY = 64;
// 下面这两个常量是修饰节点的
// MOVED 表示我正在扩容呢
static final int MOVED = -1;
// TREEBIN 表示我是一个树节点
static final int TREEBIN = -2;
// table就是存放元素的数组
transient volatile Node<K,V>[] table;
// nextTable与扩容有关,下面在说
private transient volatile Node<K,V>[] nextTable;
// baseCount记录map中的元素个数
private transient volatile long baseCount;
private transient volatile int sizeCtl;
private transient volatile int transferIndex;
存储一个节点
假设一开始我们的数组长度为16,我们在此基础上讨论如何添加一个节点。
-
最开始数组初始化好以后,数组里没有元素,此时添加第一个元素。ConcurrentHashMap与HashMap存放节点的规则一样,都是将节点的键的hash值(这里的hash值在原本的hash上又加了其他操作)和数组容量减一相与。再将相与的这个值作为节点存放的下标。第一次放节点的时候,此时这个下标处没有其他节点,所以我们就将节点放在这个位置上。
那么想必有人就有疑问了,这块不加锁还怎么保证线程安全呢。
但是完全不必担心,当数组下标为空添加节点时,用了CAS这个轻量级的锁。当判断下标处没有节点,那么就用CAS将节点放入。就算当语句执行到判断完为空线程1时间片段到了,线程2又来执行添加一个元素并且添加成功,又轮到线程1来执行,那么这个时候通过CAS就会发现加入失败,因为当前下标处已经不是null了,那么线程1就会再次循环将节点加入。而且CAS语句是原子性的,完全不用担心会被打断。
-
但是当我们添加一个元素的时候,此时下标处是有元素的,那么这个时候就要加锁了。不管当前下标下的是链表还是树。因为这种情况下的添加不是一句就能完成的。
其余的与hashmap大致一样,就不过多解释。
-
添加的时候还会遇到一种情况,比如线程1正在扩容,然后线程2来添加节点了,这种情况线程2就会暂停添加元素,“帮助”线程1完成扩容,然后在将自己的节点加入表中。
但是这个帮助并不是不管线程1运行到哪了,线程2都要好人帮到底,帮你完成。假如线程1的扩容才刚开始不久就惨遭暂停那么线程2开始的时候就帮线程1完成此次扩容。如果线程1都执行了好久了,才惨遭暂停,那么线程2就不会帮助线程1。这里面还有一些需要注意的问题,我们一边看代码一边说。
putValue
我们先来看源头的put一个节点的方法。
static final int spread(int h) {
// HASH_BITS = 0X7FFFFFFF; 用自己的高16位和低16位异或得到新的低16
// 位,高16位不变。并且只要低31位。
return (h ^ (h >>> 16)) & HASH_BITS;
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
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();
// 表初始化完成以后,才开始向表里添加数据,从这开始将节点分为
// 三大类情况 1. 下标处没有节点 2. 下标处的节点为MOVED
// 3. 下标处的节点不为MOVED,下标处的是链表或者树
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
// 如果下标处是链表形式的,就遍历链表,如果有节点与加入
// 节点相同,那么替换。否则加在末尾
// 通过fh可以判断是链表还是树,
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, 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;
}
}
}
}
// 通过binCount的大小来判断是否需要树化。
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 这个方法用来执行扩容,参数一表示每向表里加入一个节点,那么baseCount+1;
// 当baseCount的值大于等于阈值,那么表就会扩容。
addCount(1L, binCount);
return null;
}
看完基本的添加一个节点,我们再来看一下树化的过程,与之前的hashMap略有不同。
链表树化
// 参数是表和要树化的链表的下标
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// 当链表的长度大于等于8,并且数组的长度大于等于64才会树化。
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// 否则会先扩容,这个扩容也很牛批了,一次性扩大8倍
// 比如当前容量是16,那么扩容之后是128.真正的扩容操作是
// transfer方法,tryPresize方法调用了它。
// 而transfer扩容时,一次扩大两倍,从16到32
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
下面这个方法是TreeBin的构造方法,而TreeBin继承了Node。上面将当前下标处的链表里的所有节点都转换为树节点(TreeNode,他的父类也是Node),然后生成一个TreeBin节点放到当前下标处。然而TreeBin这个节点很特殊他的hash值是TREEBIN,也就是-1 。然后键值什么都没有。但是这个节点有根节点,在初始化TreeBin的时候,树就形成了。
树化的过程与hashmap一样,这里就不过多解释。
所以如果某个下标处是TreeBin那么我获得它的hash值的时候,就会是-1. 所以前面才会用hash值的正负来区分是链表还是树。
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
}
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
TreeBin(TreeNode<K,V> b) {
super(TREEBIN, null, null, null);
this.first = b;
TreeNode<K,V> r = null;
for (TreeNode<K,V> x = b, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (r == null) {
x.parent = null;
x.red = false;
r = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = r;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
r = balanceInsertion(r, x);
break;
}
}
}
}
this.root = r;
assert checkInvariants(root);
}
扩容transfer方法
这个方法虽然看起来好长啊,但是干的事并不复杂。
加入我们现在表的容量是16,然后我们将容量扩容到32. 首先先重新申请一个新的数组nextTable,容量是32 。然后根据新的数组生成一个ForwardingNode(fn),
然后从后往前遍历table,将每一个下标里都放fn。如果下标处啥都没有直接放。如果下标处有东西,那么就将此处的节点们重新按要求放到新的表里nextTable。然后再将fn放到此下标下。
放完之后,不急着走,先将table检查一遍,保证每一个下标都是fn,然后再将nextTable重新赋值给table。
那么有人可能就会问了,那万一两个线程,线程1扩容,线程2添加节点,那不会打乱表吗。不会的。
首先我们扩容的时候结合了table与nextTable,有人注意到在扩容和添加节点的时候加锁加的锁是什么锁了吗?锁是当前下标处的节点。那么意味着不管是扩容还是添加节点,只要一方再对这个节点进行操作,那么另一方就不会再对这个下标处的任意一个节点进行操作。
那么我想又会有人说,前面不是说扩容时如果当前下标处的节点处理完之后就会将下标处的节点变为fn,那如果此时有线程要在此下标处添加节点不就错了么。并不会,
因为如果我当前下标处的节点变为fn那么添加节点的时候就会检查出来当前下标的hash值为MOVED,那么该线程有两种可能,一种是去帮助线程1完成扩容。要不然就返回nextTable,将该节点重新加入nextTable里。
就算我检查MOVED时当前下标的节点还未变成fn,但在我进锁之前节点变为了fn,那也不必慌张,因为进锁之后会有重新检查当前下标里的节点是不是我之前取出来的节点,很明显不是,那么添加节点就会失败,然后重新循环添加节点。
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
// MOVED = -2;通过这个数值就可以确定此时正在扩容。
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
}
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
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;
}
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;
}
}
}
}
}
}