这篇是介绍ConcurrentHashMap的第三篇,第一篇主要介绍了在jdk1.8中所用到的一些关键知识点,第二篇主要学习了ConcurrentHashMap的组织结构与线程安全的实现,同时介绍了几个极其重要的内部类。这一篇主要是我学习领悟到的几个核心方法,包括扩容,添加和查找。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290
transfer方法(扩容方法)
再这之前,我大致描述一下扩容的过程:首先有且只能由一个线程构建一个nextTable,这个nextTable主要是扩容后的数组(容量已经扩大),然后把原table复制到nextTable中,这个过程可以多线程共同操作。但是一定要清楚,这个复制并不是简单的把原table的数据直接移动到nextTable中,而是需要有一定的规律和算法操控的(不然怎么把树转化为链表呢)。
再这之前,先简单说下复制的过程:
数组中(桶中)总共分为3种存储情况:空,链表头,TreeBin头
①遍历原来的数组(原table),如果数组中某个值为空,则直接放置一个forwordingNode(上篇博文介绍过)。
②如果数组中某个值不为空,而是一个链表头结点,那么就对这个链表进行拆分为两个链表,存储到nextTable对应的两个位置。
③如果数组中某个值不为空,而是一个TreeBin头结点,那么这个地方就存储的是红黑树的结构,这样一来,处理就会变得相对比较复杂,就需要先判断需不需要把树转换为链表,做完一系列的处理,然后把对应的结果存储在nextTable的对应两个位置。
在上一篇博文中介绍过,多个线程进行扩容操作的时候,会判断原table的值,如果这个值是forwordingNode就表示这个节点被处理过了,就直接继续往下找。接下来,我们针对源码逐字逐句介绍:
/**
* Moves and/or copies the nodes in each bin to new table. See
* above for explanation.
*/
private final void transfer(Node[] tab, Node[] nextTab) {
int n = tab.length, stride; //stride 主要和CPU相关
//主要是判断CPU处理的量,如果小于16则直接赋值16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating只能有一个线程进行构造nextTable,如果别的线程进入发现不为空就不用构造nextTable了
try {
@SuppressWarnings("unchecked")
Node[] nt = (Node[])new Node,?>[n << 1]; //把新的数组变为原来的两倍,这里的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用于多线程之间的共同扩容情况
ForwardingNode fwd = new ForwardingNode(nextTab);
boolean advance = true; //遍历的确认标志
boolean finishing = false; // to ensure sweep before committing nextTab
//遍历每个节点
for (int i = 0, bound = 0;;) {
Node f; int fh; //定义一个节点和一个节点状态判断标志fh
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//下面就是一个CAS计算
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;
//如果原table已经复制结束
if (finishing) {
nextTable = null; //可以看出在扩容的时候nextTable只是类似于一个temp用完会丢掉
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); //修改扩容后的阀值,应该是现在容量的0.75倍
return;//结束循环
}
//采用CAS算法更新SizeCtl。
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
}
}
//CAS算法获取某一个数组的节点,为空就设为forwordingNode
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//如果这个节点的hash值是MOVED,就表示这个节点是forwordingNode节点,就表示这个节点已经被处理过了,直接跳过
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
//对头节点进行加锁,禁止别的线程进入
synchronized (f) {
//CAS校验这个节点是否在table对应的i处
if (tabAt(tab, i) == f) {
Node ln, hn;
//如果这个节点的确是链表节点
//把链表拆分成两个小列表并存储到nextTable对应的两个位置
if (fh >= 0) {
int runBit = fh & n;
Node lastRun = f;
for (Node 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 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(ph, pk, pv, ln);
else
hn = new Node(ph, pk, pv, hn);
}
//CAS存储在nextTable的i位置上
setTabAt(nextTab, i, ln);
//CAS存储在nextTable的i+n位置上
setTabAt(nextTab, i + n, hn);
//CAS在原table的i处设置forwordingNode节点,表示这个这个节点已经处理完毕
setTabAt(tab, i, fwd);
advance = true;
}
//如果这个节点是红黑树
else if (f instanceof TreeBin) {
TreeBin t = (TreeBin)f;
TreeNode lo = null, loTail = null;
TreeNode hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode p = new TreeNode
(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;
}
}
//如果拆分后的树的节点数量已经少于6个就需要重新转化为链表
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin(hi) : t;
//CAS存储在nextTable的i位置上
setTabAt(nextTab, i, ln);
//CAS存储在nextTable的i+n位置上
setTabAt(nextTab, i + n, hn);
//CAS在原table的i处设置forwordingNode节点,表示这个这个节点已经处理完毕
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
PUT方法
再这之前,先简单说一下PUT的具体操作:
①先传入一个k和v的键值对,不可为空(HashMap是可以为空的),如果为空就直接报错。
②接着去判断table是否为空,如果为空就进入初始化阶段。
③如果判断数组中某个指定的桶是空的,那就直接把键值对插入到这个桶中作为头节点,而且这个操作不用加锁。
④如果这个要插入的桶中的hash值为-1,也就是MOVED状态(也就是这个节点是forwordingNode),那就是说明有线程正在进行扩容操作,那么当前线程就进入协助扩容阶段。
⑤需要把数据插入到链表或者树中,如果这个节点是一个链表节点,那么就遍历这个链表,如果发现有相同的key值就更新value值,如果遍历完了都没有发现相同的key值,就需要在链表的尾部插入该数据。插入结束之后判断该链表节点个数是否大于8,如果大于就需要把链表转化为红黑树存储。
⑥如果这个节点是一个红黑树节点,那就需要按照树的插入规则进行插入。
⑦put结束之后,需要给map已存储的数量+1,在addCount方法中判断是否需要扩容
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//key和value都不可为空,为空直接抛出错误
if (key == null || value == null) throw new NullPointerException();
//计算Hash值,确定数组下标,这个和HashMap是一样的,我再HashMap的第一篇有介绍过
int hash = spread(key.hashCode());
int binCount = 0;
//进入无线循环,直到插入为止
for (Node[] tab = table;;) {
Node f; int n, i, fh;
//如果table为空或者容量为0就表示没有初始化
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(hash, key, value, null)))
break; // no lock when adding to empty bin这句话的意思是这个时候插入不用加锁
}
//如果在插入的时候,节点是一个forwordingNode状态,表示正在扩容,那么当前线程进行帮助扩容
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 e = f;; ++binCount) {
K ek;
//如果遍历到一个值,这个值和当前的key是相同的,那就更改value值
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node pred = e;
//如果遍历到结束都没有遇到相同的key,且后面没有节点了,那就直接在尾部插入一个
if ((e = e.next) == null) {
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
//如果是红黑树存储就需要用红黑树的专门处理了,笔者不再展开。
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//判断节点数量是否大于8,如果大于就需要把链表转化成红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//map已存储的数量+1
addCount(1L, binCount);
return null;
}
其实,相对于transfer来说,PUT理解起来是不是简单很多?说到transfer,咋在PUT方法中都没出现过,只有一个helpTransfer(协助扩容)方法呢?其实,transfer方法放在了addCount方法中,下面是addCount方法的源码:
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
//是否需要进行扩容操作
if (check >= 0) {
Node[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
//如果小于0就说明已经再扩容或者已经在初始化
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//如果是正在扩容就协助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//如果正在初始化就首次发起扩容
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
GET方法
Get方法不论是在HashMap和ConcurrentHashMap都是最容易理解的,它的主要步骤是:
①先判断数组的桶中的第一个节点是否寻找的对象是为链表还是红黑树,
②如果是红黑树另外做处理
③如果是链表就先判断头节点是否为要查找的节点,如果不是那么就遍历这个链表查询
④如果都不是,那就返回null值。
public V get(Object key) {
Node[] tab; Node e, p; int n, eh; K ek;
int h = spread(key.hashCode());
//数组已被初始化且指定桶中不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//先判断头节点,如果头节点的hash值与入参key的hash值相同
if ((eh = e.hash) == h) {
//头节点的key就是传入的key
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;//返回头节点的value值
}
//eh<0表示这个节点是红黑树
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;//直接从树上进行查找返回结果,不存在就返回null
//如果首节点不是查找对象且不是红黑树结构,那边就遍历这个列表
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
//都没有找到就直接返回null值
return null;
}
好啦,ConcurrentHashMap我已经把自己学习到的都写出来了,其实还有一些东西我没能来得及写出来,可能写的时候,脑子完全被一个方法或者一个操作牵引住了,思维扩散不开。再者,博主也有很多没有理解的地方,比如说在扩容的过程中,把一个链表拆分为两个链表到底是一个怎么样的过程,在HashMap的推理了一遍没有理解,在ConcurrentHashmap也推理了一遍,还是没有理解…各位无意间浏览到的大神,可以帮我指点一二嘛?