1.回顾
我们知道,当一个桶位形成了红黑树时,此时桶位中存的是一个TreeBin节点,其内部存在两个引用,分别是指向双向链表的first
和指向红黑树根节点的r
。
2.属性
/*
* TreeBin继承了Node。其也有hash值(为-2)
*/
static final class TreeBin<K,V> extends Node<K,V> {
//红黑树根节点
TreeNode<K,V> root;
//双向链表的头结点
volatile TreeNode<K,V> first;
//等待者线程(当前lockState是读锁状态)
volatile Thread waiter;
/*
* 1.写锁状态,写是独占状态,以散列表来看,真正进入到TreeBin中的写线程,同一时一
* 只有一个线程。 lockState = 1;
*
* 2.读锁状态,读锁是共享,同一时刻可以有多个线程同时进入到TreeBin对象中获取数据
* 每一个读线程都会给lockState + 4;
*
* 3.等待者状态(写线程在等待),当TreeBin中有读线程目前正在读取数据时,写线程无法
* 修改数据,那么就将lockState的最低两位设置为 10(二进制)
*/
volatile int 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
3.treeifyBin方法
前面我们在分析put方法时,当我们成功的添加了数据时,会检查链表的长度是否大于8,大于的话会调用treeifyBin()方法。
TreeBin
的成员方法,将链表转换为红黑树的方法.
流程总结
1.先检查当前哈希表的长度是否达到了MIN_TREEIFY_CAPACITY(64),如果没有达到,则会去尝试扩容而不会将当前桶位转为红黑树。
2.第一步会先将当前桶位的头节点锁定(synchronized)
,然后遍历链表,将原Node链表转换为TreeNode形成的双向链表
。
3.最后通过传入双向链表的头结点构造一个TreeBin节点 (调用TreeBin的构造方法),放到当前的桶位上。
源码解析
private final void treeifyBin(Node<K,V>[] tab, int index) {
/*
* b 桶位的头节点
* n 哈希表的长度
* sc sizeCtl的值
*/
Node<K,V> b; int n, sc;
if (tab != null) {
//数组长度未达到树化的最小长度(64),此时不进行树化,尝试去扩容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
/*
* 条件成立,说明当前桶位有数据,且是普通Node节点。
* (hash值大于等于0,说明就是普通Node节点)
*/
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
//锁定当前桶位(锁对象是桶位的头元素)
synchronized (b) {
//再次判断,防止多线程下加锁对象已经被修改
if (tabAt(tab, index) == b) {
/*
* 接下来就是将普通的单向Node链表转换为双向的TreeNode链表
* 为了便于树化
* hd 指向双向链表的头几点,tl指向双向链表的尾结点
*/
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;
}
/*
* 将桶位上的原单向链表构造成一个TreeBin对象
* 将双向链表(TreeNode)的头结点传入,构造出一颗红黑树。
* 将构造出来的TreeBin节点放到当前桶位上。
*/
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
4.构造方法
接着上面的treeifyBin()方法,传入一个双向链表构造出一颗红黑树。
过程
- 先调用父类Node的构造器设置
TreeBin节点的hash值为-2
,然后TreeBin内部的first属性引用传来的双向链表的头节点。 - 然后遍历双向链表,根据节点的hash值,hash值比当前节点小的放到往左子树插,反之往右子树插。最终构造出一颗红黑树。
- 将构造出来的红黑树的根节点赋值给TreeBin内部的root引用。
/*
* 将双向TreeNode链表转换为一颗红黑树
* 传来的这个参数b是构造成的一个双向链表的头结点
*/
TreeBin(TreeNode<K,V> b) {
//调用父类的构造器,只设置了自己的hash值为-2,(TREEBIN = -2)
super(TREEBIN, null, null, null);
//使用first引用TreeNode链表。
this.first = b;
//r就是构造后的红黑树的根节点
TreeNode<K,V> r = null;
/*
* x表示遍历的当前节点
* next表示x的next节点
*/
for (TreeNode<K,V> x = b, next; x != null; x = next) {
//拿到next节点
next = (TreeNode<K,V>)x.next;
//强制设置当前插入节点的左右子树为NULL。
x.left = x.right = null;
/*
* 第一个节点进来就是根节点。
* 条件成立,说明当前红黑树是一个空树,
* 那么设置插入元素为根节点
*/
if (r == null) {
//根节点的父节点为NULL
x.parent = null;
//红黑树的根节点是黑色
x.red = false;
//r引用x的对象 (根节点的对象)
r = x;
}
//非第一个节点进来,此时已经有根节点了。
else {
//k 表示插入节点的key
K k = x.key;
//h表示插入节点的hash
int h = x.hash;
//kc 表示插入节点key的Class类型
Class<?> kc = null;
//表示为查找插入节点的父节点的一个临时节点
TreeNode<K,V> p = r;
//自旋
for (;;) {
/*
* dir(两种值 -1, 1)
* -1表示插入节点的hash值大于当前p节点的hash
* 1表示插入节点的hash值小于当前p节点的hash
* ph 表示查找过程中p的hash值。
*/
int dir, ph;
//临时节点p的key。
K pk = p.key;
//--- 下面三种情况都是为dir赋值
/*
* CASE1
* 待插入节点的hash值小于当前节点
* 待插入节点可能需要插入到当前节点的左子节点
* 或 继续在左子树上查找
*/
if ((ph = p.hash) > h)
dir = -1; // dir设置为 -1
/*
* CASE2
* 待插入节点的hash值大于当前节点
* 待插入节点可能需要插入到当前节点的右子节点
* 或 继续在右子树上查找
*/
else if (ph < h)
dir = 1; // dir设置为1
/*
* CASE3
* 待插入节点hash值与当前节点hash值一致,会在case3做出最终排序,
* 最终拿到的dir一定不是0,一定是(-1,或1)
*/
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
//xp 表示的是插入节点的父节点
TreeNode<K,V> xp = p;
/*
* 根据dir最终的值表明向左或者向右搜索插入位置
*
* 条件成立,说明当前p节点即为插入节点的父节点,即当前节点就是叶子节点,可以将节点插入进去
*
* 条件不成立,说明p节点下面还有及诶单,需要将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;
}
}
}
}
//将r赋值给TreeBin对象的root引用。
this.root = r;
assert checkInvariants(root);
}
5.find方法
/*
* h表示查找元素的hash值
* k 表示查找元素的key
*/
final Node<K,V> find(int h, Object k) {
if (k != null) {
/*
* e表示循环迭代的当前节点
* 迭代的是first引用的链表。
*/
for (Node<K,V> e = first; e != null; ) {
/*
* s保存的是lock的临时状态
* ek 链表节点当前节点的key
*/
int s; K ek;
//条件成立表示当前TreeBin有等待者线程 或者 有写操作线程正在加锁
if (((s = lockState) & (WAITER|WRITER)) != 0) {
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
}
/*
* 前置条件: 当前TreeBIn没有等待者线程或写线程
*
* 这里CAS尝试添加读锁,锁状态+4
*/
else if (U.compareAndSwapInt(this, LOCKSTATE, s,
s + READER)) {
/*
* r 红黑树根节点
* p 最终查询到的节点
*/
TreeNode<K,V> r, p;
try {
p = ((r = root) == null ? null :
r.findTreeNode(h, k, null)); //查询
} finally {
Thread w;
/*
* 当前线程查询红黑树结束,尝试释放当前线程的读锁,就是让
* lockState - 4
*
* (READER|WAITER) = 1010 表示当前只有一个线程在读,且
* 只有一个线程在等待 当前读线程为TreeBin中的最后一个读线程
*
* (w = waiter) != null 说明有一个写线程在等待读操作全部结束
*
* 大致意思就是说,当前的这个读线程是最后一个读线程,并且发现当前有写线程在等待(阻塞),
* 此时的这个读线程读完之后需要将写线程唤醒。
*/
if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
(READER|WAITER) && (w = waiter) != null)
//unpark唤醒写线程,
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}