准大四生为了秋招,刚好也方便复习写一写,希望各位大佬或者面试官看到了能指出不对地方
本文依然从最常用的put方法入手,我也看到很多博客的put方法的讲解,我就先简单讲一些大家都讲的东西,然后再讲一些不是每个人都讲的,例如 ** addCount(1L, binCount);*方法和树插入还有helpTransfer等等。
jdk版本1.8 由于没用过1.7也没看过。只是再网上看过一些博客 就不讲1.7了
先来说一下总体操作吧:
1 死循环变量hash桶也就是table,然后第一次插入就初始化table (等下讲初始化方法)。
2 然后如果算出hash桶中的位置如果为空就用cas插入(cas就是的原理应该是利用对总线上锁,某一时刻只能有一个cup对数据修改成功。其他线程再此修改就会发现 预期值和旧值不等,修改失败)
3 然后判断当前hash是否等于MOVED的这个状态表示此时数组正在扩容,我们这个线程要去帮助扩容,也就是说支持多线程扩容(等下讲这个方法)
4 然后就是锁住hash桶的头节点 进入插入操作 该更新就执行更新操作,该插入就插入,如果是树就执行树的插入操作(这里的红黑树是用的TreeBin TreeBin里面封装了 TreeNode为什么这样做了 是因为一棵红黑树的插入操作可能会引起根节点的变化 也就是hash桶的头节点,而我们锁住的正是头节点,一旦头节点一变锁的对象就不是头节点了,而用Treebin来封装了TreeNode 保证每次头节点都是同一个Treebin对象。)
5 然后最后判断前面技术bincount是否大于阈值 ,大于就树化(当然容量必须大于64才树化否则扩容)
6 然后进行增加size操作(这里也对多线程采取了优化, 里面通过一个CounterCell数组和basecount值相加。等下详细讲)
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); //算出hash值
int binCount = 0; //统计链表长度
for (Node<K,V>[] tab = table;;) {
//一个for的死循环
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); //第一次插入 hash桶为空 初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//算出当前的hash桶位置没有元素 直接利用cas插入
//cas就是的原理应该是利用对总线上锁,某一时刻只能有一个cup对数据修改成功。
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//这个方法等下讲 如果进入这个条件 表示当前map正在扩容 我应该去帮助他扩容。也就是多线程扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//前面条件都不满足 开始进入插入操作,对hash桶的头节点上锁
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;
//这里相当于是更新操作 也就是原来插入的key是有的
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;
}
}
}
//这里判断是树 hashmap里面用的树是TreeNode 这里用的是TreeBin TreeBin里面封装了 TreeNode为什么这样做了 是因为一棵红黑树的插入操作可能会引起根节点的变化 也就是hash桶的头节点,而我们锁住的正是头节点,一旦头节点一变锁的对象就不是头节点了,而用Treebin来封装了TreeNode 保证每次头节点都是同一个Treebin对象。
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//这里的树的插入在我前面讲过的文章hashmap里面又是差不多的 就不在这里继续讲了 有兴趣可以去看一下。
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;
}
}
}
//这里就是增加size的方法 里面通过一个CounterCell数组和basecount值相加。等下详细讲
addCount(1L, binCount);
return null;
}
为了衔接上次讲的hashmap 树化过程 这里先讲 concurrenthashmap的书画过程 其实大同小异
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
//树化肯定不能释放头节点的锁,在前面的插入代码完成他释放了锁 所以这里需要重新获取
synchronized (b) {
//下面就和前面讲的hashmap一样 如果没看过可以看一下
//也就是把单向链表转换成双向链表
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;
}
//利用cas 吧tab的index位置改成了 Treebin数据结构包装的红黑树
//前面已经讲过为什么用Treebin而不是TreeNode防止插入时红黑树头节点变化导致所致的头节点变化了
//newTreebin过程设计了 红黑树的插入旋转啊变色 这些都是在前面hashmap讲过就不重复了
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
盘点hash桶是否为空 如果为空 并且 sizeCtl(默认等于0)不小于0 就利用cas修改为-1
最终把他修改为 0.75*n 如果在初始化过程的过程中来了 线程就调用yiled 让出cup执行权限当然自己也会去参与抢夺cup权限的过程。在前面那个线程初始化成功 新的来线程之间返回 ,yiled的线程 进入下面esle if 然后break 最后返回
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;
}
不知道多少人跟我再没看增加size的方法直接一样 以为就是利用原子类的size或者加锁实现了size的++
果然大神的思路就是跟我这个大菜b不一样。言归正传
先说一下思路 利用一个CounterCell [ ] 数组和一个basecount(这个相当于基础的size) 当一个线程正在修改basecount的时候 其他线程去通过一个hash算法 算出再CounterCell [ ]中的位置 然后对对应位置利用cas修改对应位置的值 最后统计size的时候 遍历CounterCell [ ]和basecount加起来.
每一个CounterCell 里面又一个value 每次相当于也锁住的是数组对应的位置而不是锁住整个数组 这就是他并发量高的原因。
思路就是这样 思路明白了以后
接下来进入源码分析
private final void addCount(long x, int check) {
//x传入进来的是1 check就是前面的
//bincount
//这里就是前面说数组
CounterCell[] as; long b, s;
//注意这里判断 第一次添加size counterCells肯定为空 如果counterCells不为空他就不会修改basecount了而是直接修改counterCells对应位置的值
if ((as = counterCells) != null ||
//这里利用cas修改basecount的值 如果修改失败 也就是别的线程正在修改才会进入下面代码
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
//这里的意思是 如果CounterCell数组为空,或者说 CounterCell数组算出来下标对应位置为空
//或者说 在算出下标对应位置利用cas修改失败 就会进入fulladdCount方法
//ThreadLocalRandom.getProbe() & m这个代码每次算出来的值对于同一个线程都行一样的
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();
}
//这里是没有走上面return的代码
//check也就是前面的bincount 什么时候会小于0了
// 当然是remove一个key的时候 这里是插入 只要前面没return 都会走这个代码
//下面其实就是扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//这里的sizeCtl在初始化的时候被修改成了 阈值也就是 平衡因子*容量 0.75*16=12
//如果说s(也就是目前位置的总的size数目)大于了阈值 并且hash桶长度小于最大长度就扩容。
//为什么用while不用if了因为 并发量高的情况下扩容完毕可能又满了又要继续扩容 我们这个线程又可以继续扩容
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
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);
}
//利用cas修改成一个非常小的值 用于统计帮助转移的线程数量
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//开始转移
transfer(tab, null);
//重新统计总的size
s = sumCount();
}
}
}
**
**
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
//默认数组冲突是false
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
//这里是CounterCell数组不为空 ,于是先看下面那个分支为空的情况,第一个线程进来肯定为空嘛 先看 下面的为空情况简单
if ((as = counterCells) != null && (n = as.length) > 0) {
//这里就是判断 算出对应下标数组中的元素是否为空 这里是为空
//为空肯定就要放入一个CounterCell 对象进去嘛 所以就会修改cellBusy为1
//表示正在被使用其他线程不能修改 然后再对象位置new 一个CounterCell 对象
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) {
// Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try {
// Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
//这个参数与扩容有关 相当于拿来判断当前线程修改成功的次数
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//代表数组中所有元素都初始化过 就可以修改对应下标位置的值了 不是锁住整个数组而是锁住对应下标
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
else if (!collide)
collide = true;
//这里对CounterCell 数组扩容 到这里来的原因是我循环了两次对应修改都没成功就扩容了
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {
// Expand table unless stale
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
//这里是CounterCell数组为空 代表还没被初始化,cellsBusy 为0代表没有线程
//正在使用这个数组 于是就用cas修改为1 不准别的线程再来初始化了
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try {
// Initialize table
if (counterCells == as) {
//开始初始化数组
CounterCell[] rs = new CounterCell[2];
//并且把对应位置的值修改成x也就是1 然后就退出循环了。因为值已经插入进去了
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
//把数组修改为没有正在使用状态
cellsBusy = 0;
}
//退出循环
if (init)
break;
}
//如果数组正在初始化 则尝试修改basecount的值 肯定不会傻傻等待把
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
先讲一下扩容的总体思路:
扩容的时候 会算出一个步长 根据cpu的核心数量来算,最小为16 现在为了方便 假设 数组数量为4 步长为2 那么就会通过计算算出A线程的步长是2开始位置是下标是3 2 默认是从数组从右像左扩容的 B线程来了算出的位置就是1 0 然后把对应位置转移过去,并且会把对应位置更新为一个forwardingNode 他的hash值为1 这也是put操作中判断正在扩容并且帮助扩容的重要标志。
transferIndex=4
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//这里就是算步长,根据cpu核心数 最小为16
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;//这个变量代表了当前线程是否需要继续扩容
//假设当然map容量为4 步长为1 只要A线程扩容每次扩容一个 那么下一次肯定是要继续扩容的
//但是也有可能有多个线程同时扩容 我扩容完了 其他线程把整个也扩容完了就不需要前进,继续扩容了后面会修改为false
//判断当前线程的工作是否完成 跟前面一样 假设只要A线程再扩容 那么我肯定还需要再继续工作,
//但是如果其他线程正在扩容其他的 我就完成了 就修改完true
boolean finishing = false; // to ensure sweep before committing nextTab
//这里的I和bound带变了 每次扩容的边界
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
//如果i减一以后还是>=bound说明还在步长内不需要继续前进扩容
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
//下一个边界都小于=0了说明也不继续前进了
i = -1;
advance = false;
}
//第一次回进入到这里 假设map容量为4 步长为2
//TRANSFERINDEX再前面看到是等于n=4的
//nextIndex=TRANSFERINDEX=4
//nextBound=4-2=2;
//i=nextIndex=1=3
//同时cas修改 TRANSFERINDEX为nextBound
//现在明白了把 就是这样算的
//假设其他线程也修改 他们获得的TRANSFERINDEX是2 注意看上面的else if 这时候
//来了一个线程B获取到的TRANSFERINDEX就是2 算出的i和bound就是1 和0
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;
}
//判断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
}
}
//如果说当前位置为空 说明不需要转移 之间设置fwd 防止其他线程这时候插入新值
//扩容的时候肯定不能插入嘛
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//跟null一样的道理
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;
}
}
}
}
}
}
再前面put方法里面说过了这个方法用来帮助扩容 再前面也说过 扩容的时候回把sizeCtl改成非常小的一个值用来统计帮助线程扩容数量
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) {
//同样扩容的时候会把sizeCtl修改成一个非常小的值
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
//这里就用cas来统计如果线程帮助扩容就 扩容线程数量+1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);//真正帮助开始扩容 这个方法前面已经讲过
break;
}
}
return nextTab;
}
return table;
}
写的不好的地方请各位大神 指出,制作不易 每次写 4 5个小时 如果觉得还行 麻烦给个赞