本文就ConcurrentHashMap红黑树的读写逻辑,尤其是jdk怎么控制读写同步的逻辑梳理下
本文要讨论的问题主要分两种
1 有线程在读,写进程怎么办?
2 有线程在写,读进程怎么办?
TreeBin的hash值 -2哦
static final int TREEBIN = -2; // hash for roots of trees
一 有线程在读,写进程怎么办?
先来看看
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; // values for lockState static final int WRITER = 1; // set while holding write lock static final int WAITER = 2; // set when waiting for write lock static final int READER = 4; // increment value for setting read lock
lockState至关重要,读红黑树和写红黑树都会通过CAS改lockState。
读操作发现此时 lockState = 0;会通过cas对它加4
再写之前会有竞争锁的操作
private final void lockRoot() { if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER)) contendedLock(); // offload to separate method
如果有读进程lockState肯定不是0,CAS失败,进入 contendedLock()。
private final void contendedLock() { boolean waiting = false; for (int s;;) { //这是一个自旋 if (((s = lockState) & ~WAITER) == 0) {//只有读线程退出了 也就是lockState = 0,该if才可能是0,所以第一次不可能进来 if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {// if (waiting) waiter = null; return; } } else if ((s & WAITER) == 0) { //假设有一个读线程 s = 4, 0100,WAITER = 2,0010,与的结果就是0,第一次是最可能进到这个分支 if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) { //此时LOCKSTATE的值第二位肯定是1,因为任何一个数和 0010或,1那位结果就是1 waiting = true; waiter = Thread.currentThread(); } } else if (waiting) LockSupport.park(this); } }
注意这个是一个自旋操作,自旋操作往往第一次不会有结果。
所以,第一次自旋发现有读线程,因为 LOCKSTATE 不等于0。这时候会把第二位置为1。然后
waiting = true; waiter = Thread.currentThread();
然后第二次自旋,如果第二次自旋发现,读线程还是没有释放。很显然就会走到
else if (waiting) LockSupport.park(this);
写进程必须挂起。
所以,结论就是如果有读线程,写线程必须挂起。把线程本身的引用,赋值给 TreeBin的waiter,并等待唤醒。
现在我们看看读线程退出了,是怎么唤醒的。
final Node<K,V> find(int h, Object k) { if (k != null) { for (Node<K,V> e = first; e != null; ) { int s; K ek; if (((s = lockState) & (WAITER|WRITER)) != 0) { if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; e = e.next; } else if (U.compareAndSwapInt(this, LOCKSTATE, s, s + READER)) { TreeNode<K,V> r, p; try { p = ((r = root) == null ? null : r.findTreeNode(h, k, null)); } finally { Thread w; if (U.getAndAddInt(this, LOCKSTATE, -READER) == (READER|WAITER) && (w = waiter) != null) LockSupport.unpark(w); } return p; } } } return null; }
先不看其他的代码,看 finally
U.getAndAddInt(this, LOCKSTATE, -READER) == (READER|WAITER)
getAndAddInt会先得到旧值,然后再减去4,也就是 加上 -READER。
如果这个值就是110 (100 | 10),那就说明它是最后的读线程,同时说明有等待中的写线程。
所以执行唤醒。
以上我们就分析完了在有读线程的情况下,写线程的处理逻辑。
二 有线程在写,读线程怎么办?
这种情况相对要容易很多。代码就在上贴过的find
if (((s = lockState) & (WAITER|WRITER)) != 0) { if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; e = e.next; }
如果发现 (s = lockState) & (WAITER|WRITER) 不为0.那就说明 要么有线程在写,要不就是有线程等待要写。
所以此时就退化为按照链表的方式进行查找。