本文主要从个人角度着重讲解了ConcurrentHashMap的部分代码。从源码的角度出发,大胆猜测并解释作者的意图。从而学习作者的在代码实现上的优秀思想。
文章目录
一、关键属性
/**
* 默认初始表容量,必须是2的幂(即至少1)。
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* 最大可能的(非2的幂)数组大小。减8的原因是因为需要一些空间存放数组默认属性。
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 扩容因子,实际上,ConcurrentHashMap在扩容时,不会等到实际定义的实际容量的时候才去扩容,
* 而是容量达到实际容量的0.75倍时,就已经开始扩容。
* eg:如果我们定义的实际容量为16,当放满12(16 * 0.75)个元素的时候就开始扩容。
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* 树化时的阈值,链表容量达到8个长度的时候, 就开始转化为红黑树。
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 树的长度小于6的时候, 就开始转化为链表。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 重要:这个参数有三种情况:
* 1、sizeCtl = -1:说明正在初始化表
* 2、sizeCtl = -N: 代表有X个线程正在进行扩容操作。这里的X取-N对应的二进制的低
* 16位数值为M,此时有M-1个线程进行扩容
* 3、其余情况:如果表已经创建完成,代表需要扩容的阈值(容量的0.75倍)。
* 如果表未创建,代表需要初始化的表的容量。
*/
private transient volatile int sizeCtl;
二、关键方法
1.ConcurrentHashMap(int initialCapacity)
带参构造函数,参数为数组的大小,但是该指定的参数并不一定是数组的实际大小。
/**
* 带参构造函数
*/
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
/**
* initialCapacity是我们指定的容量,如果该容量>=最大容量的一半,那么指定的容量就是最大
* 容量,否则,就找大于(指定容量 + (指定容量/2) + 1)的最近的2的次幂数为指定容量。
* eg: 如果指定容量为8,那么(8 + 8/2 + 1)=13,那么最终初始容量为16。
*/
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
/**
* 找大于(指定容量 + (指定容量/2) + 1)的最近的2的次幂数为指定容
* 量。tableSizeFor这个函数多次测试发现就是该含义。这里和HashMap有区别,
* HashMap直接会将指定容量传进去计算,那么找的就是离指定容量的最近的2
* 的次幂数为指定容量
*/
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
2. putVal(K key, V value, boolean onlyIfAbsent)
调用put()方法的时候,调用该方法进行数据的插入。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
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();
/**
* 表创建好之后,开始存放第一个元素,首先会判断我们的第i个位置上是否已经存在数据了,
* 如果不存在,则在该位置上直接new Node<K,V>(hash, key, value, null)放在
* 数组第i个位置上。其中 i = (n - 1) & hash)实际上就是在计算该元素
* 在数组中的下标,等效于 i = (tabel.length - 1) & hash计算后得到的值为数组下标。
*/
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
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;
/**
* 开始循环第i个位置上的链表
*/
for (Node<K,V> e = f;; ++binCount) {
K ek;
/**
* 如果第i个位置上的元素的key和新put的元素的key是一样的,
* 则直接将旧值保存起来,后用新值覆盖旧值,key还是之前的
* 那个key,并且将旧值返回(put是有返回值的)。
*/
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
/**
* 如果发生了hash冲突,且key相同,保存旧值,直接用新值
* 替换旧值
*/
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
/**
* 如果第i个位置上的元素的指针next(单向链表
* 特性)是null,则说明是已经循环到了i位置上的最
* 后一个链表元素位置,那么直接将新的key-value插入到此处即可。
* pred(e)的next指向新的key-value的node节点即可,并且
* 该node节点的next为null。
*/
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
/**
* 如果第i个位置上的元素是一颗红黑树,调用putTreeVal将新的key-value
* 插入红黑树的节点,这里比较复杂,因为在插入新的节点之后,要保持红黑树
* 的平衡性,要做一系列的左旋或者右旋,节点的红黑颜色的变换。
*/
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
/**
* 如果找到key相同的红黑树的节点的位置,说明发生了hash冲突,将该节
* 点返回,进行该节点值的替换操作。如果返回来的是null,说明没有发生
* hash冲突,节点插入成功。
*/
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
/**
* 如果发生了hash冲突,保存旧值,直接用新值替换旧值
*/
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
/**
* 插入新的元素后,判断下数组中第i位置上的链表容量是否超过了树化阈值,
* 如果超过了树化阈值(8),那么需要将链表转化为红黑树。
*/
if (binCount >= TREEIFY_THRESHOLD)
/**
* 树化方法
*/
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
/**
* 统计数据,判断是否需要进行扩容操作,并进行数据迁移
*/
addCount(1L, binCount);
return null;
}
3. initTable()
初始化数组。当第一次调用put()方法的时候,才会调用该方法,进行数组的初始化。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
/**
* 第一次进来,sizeCtl为计算出来的表的容量,并将值赋值到sc,那么sc也就是表的容量。
*/
if ((sc = sizeCtl) < 0)
/**
* 要保证线程安全,也就是确保只能有一个线程去初始化表。如果sizeCtl=-1小于0,
* 其他线程让出cpu,只允许第一个线程去执行创建表的操作。
*/
Thread.yield(); // lost initialization race; just spin
/**
* 第一个进来的线程执行U.compareAndSwapInt(this, SIZECTL, sc, -1)将sizeCtl更
* 改为-1,确保后续的线程让出cpu,不会进来这个分支,只有它一个在执行创建表的操作。
*/
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
/**
* 马上要创建数组了,这里确认下数组的长度,是默认值还是sc设置的值。
*/
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
/**
* 数组创建好值后,sc的值被更改为扩容时的阈值(指定的容量 * 0.75)
*/
sc = n - (n >>> 2);
}
} finally {
/**
* 数组创建好值后,ssizeCtl的值被更改为扩容时的阈值(指定的容量 * 0.75)
*/
sizeCtl = sc;
}
break;
}
}
return tab;
}
4. putTreeVal(hash, key, value)
向红黑树中插入一个新的节点。插入的时候,需要从root节点开始进行大小判断,定位该元素应该放在当前节点的左孩子位置还是右孩子位置。
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
/**
* 对于红黑树来说,必须有一个root节点,其余的子孙节点都是在这个根节点向下发展的
*/
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
/**
* 如果没有根节点root,就以待插入的key-value创建一个。
*/
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
/**
* 如果p(root)节点的key的hash值大于待插入的key的hash(h),说明该key-value一定
* 为p的左子树,否则为右子树。
*/
else if ((ph = p.hash) > h)
/**
* 标记为p的左子树。
*/
dir = -1;
else if (ph < h)
/**
* 标记为p的右子树。
*/
dir = 1;
/**
* 如果发生了hash冲突,则直接将p(root),也就是把当前节点返回。
*/
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
/**
* 如果根据hash值无法判断待插入的key应该是放在哪个位置,则用下边这种方式进行判断:
* 如果key的类型实现了Comparable接口,那么就返回key的类型的class,否则就返回null。
* (kc = comparableClassFor(k)) == null)如果返回不是true,那么调用
* mpareComparables这个方法得到一个dir值(用来决定是该节点是左子树还是右子树),
* 实际上mpareComparables方法就是调用了key的类型的class的compareTo方法。
*/
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
/**
* 如果找到了与待插入的key相同的节点,就将该节点返回,外层进行替换即可。
*/
if (((ch = p.left) != null &&
(q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
/**
* 如果上述两个方法都无法判断待插入的key应该是放在哪个位置,那么调用该方法使用
* System.identityHashCode进行判断。
*/
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
/**
* 根据dir的值来判断,待插入的值是插在左子树的位置还是右子树的位置。并将当前节点的左子树
* 或者右子树指向p
*/
if ((p = (dir <= 0) ? p.left : p.right) == null) {
TreeNode<K,V> x, f = first;
/**
* 根据待插入的key-value创建一个树节点。
*/
first = x = new TreeNode<K,V>(h, k, v, f, xp);
if (f != null)
/**
* 实际上,树节点也是一个双向链表,因为TreeNode extends Node<K,V>,
* Node里面有一个prev属性,就是指向上一个元素的。
*/
f.prev = x;
if (dir <= 0)
/**
* 如果dir<0,插在左子树的位置
*/
xp.left = x;
else
/**
* 如果dir>0,插在右子树的位置
*/
xp.right = x;
if (!xp.red)
/**
* 如果新插入的节点的父节点不是红色的,那么该节点就必须是红色的。
*/
x.red = true;
else {
lockRoot();
try {
/**
* 在插入新的节点信息后,可能会导致红黑树不平衡,所以需要对红黑树进行平
* 衡调整。
*/
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
}
break;
}
}
/**
* 验证是不是红黑树,这个是可选的,如果idea中没有配置开启assert验证,那么就不会执行。
*/
assert checkInvariants(root);
return null;
}
5. balanceInsertion(root, x)
对红黑树进行平衡调整,涉及到红黑树的左旋操作,右旋操作,以及节点的颜色的改变。
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
/**
* 设置新的节点的默认为红色。
*/
x.red = true;
/**
* 开始循环红黑树。
*/
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
/**
* 如果新增的节点的父节点是null,表明自己就是父节点,那么就红色变黑色,
* 并将新节点x返回,表明他就是这棵树的父节点。
*/
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
/**
* 如果新增的节点有父节点,且新节点的父节点就是黑色的,或者新节点的祖父节点是null,
* 那么说明红黑树就是的平衡的,直接返回root节点。
*/
else if (!xp.red || (xpp = xp.parent) == null)
return root;
/**
* 如果新增的节点有父节点,且新节点的父节点就是祖父节点的左子树
* (相当于新增节点的父节点在左子树的位置)。
*/
if (xp == (xppl = xpp.left)) {
/**
* 如果祖父节点有右子树,且右子树是红色的。
* 1、“叔叔节点”变成黑色。
* 2、“父亲节点”变成黑色。
* 3、“祖父节点”变成红色。
* 4、将祖父节点设置为当前节点,继续循环对当前节点进行调整。
*/
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
/**
* 祖父节点除了上边的分支,其他情况下。
* 如果新插入的节点刚好是父亲节点的右子树。
* 1、设置当前节点为父亲节点。
* 2、以父节点为支点左旋。
* 3、如果新插入节点的父亲节点不为null,那么设置祖父节点为新
* 插入节点的祖父节点,否则为null。
*/
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
/**
* 如果新插入节点的父节点不为null:
* 1、设置父亲节点为黑色。
*/
if (xp != null) {
xp.red = false;
/**
* 如果新插入节点的祖父节点不为null:
* 1、设置祖父节点为红色。
* 2、以祖父节点为支点右旋。
*/
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
/**
* 如果新增的节点有父节点,且新节点的父节点不是祖父节点的左子树。
*/
else {
/**
* 如果祖父节点有左子树,且左子树是红色的(相当于新增节点的父节点在右子树的位置)。
* 1、“叔叔节点”变成黑色。
* 2、“父亲节点”变成黑色。
* 3、“祖父节点”变成红色。
* 4、将祖父节点设置为当前节点,继续循环对当前节点进行调整。
*/
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
/**
* 祖父节点除了上边的分支,其他情况下。
* 如果新插入的节点刚好是父亲节点的左子树。
* 1、设置当前节点为父亲节点。
* 2、以父节点为支点右旋。
* 3、如果新插入节点的父亲节点不为null,那么设置祖父节点为新
* 插入节点的祖父节点,否则为null。
*/
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
/**
* 如果新插入节点的父节点不为null:
* 1、设置父亲节点为黑色。
*/
if (xp != null) {
xp.red = false;
/**
* 如果新插入节点的祖父节点不为null:
* 1、设置祖父节点为红色。
* 2、以祖父节点为支点左旋。
*/
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
6. rotateLeft(root, xpp)
对红黑树进行左旋调,假如这里传入的节点p为当前节点,且当前节点新插入节点的父节点(rotateLeft(root, x = xp))。
(01) 将“父节点”设为“黑色”
(02) 将“祖父节点”设为“红色”
(03) 以“父节点”为支点进行左旋
/**
* root就是整棵树的root节点。
* p是当前需要旋转的支撑点。
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
/**
* 如果旋转的支点为null,直接返回root节点。
* 如果旋转的支点不为null,设置支撑点的右孩子(p.right)为r:
* 1、设置支撑点的右孩子(p.right)指向r的左孩子(r.left)为r1,如果r1不为null,
* 那么设置r1为p。
* 2、设置r的父节点(r.parent)指向支撑点的父节点(p.parent)为pp,如果pp==null,
* 那么说明p没有父节点,直接将root节点指向r,且应该设置为黑色。
* 3、如果p==pp的左孩子节点,设置pp的左孩子节点指向r。否则,设置pp的右孩子节点指向r。
* r的右孩子节点指向p,p的父亲节点指向r。
*/
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
7. addCount(1L, binCount)
当put一个元素的时候,都会调用addCount(1L, binCount)记录元素的个数,addCount(1L, binCount)会先利用CAS更新baseCount的值,如果因为高并发导致CAS更新baseCount失败,那么会调用 fullAddCount(x, uncontended)操作counterCells数组来计数。CounterCell是counterCells数组中保存的对象,其内部有一个volatile的value属性,当put一个元素的时候,都会调用addCount(1L, binCount)记录元素的个数,addCount(1L, binCount)会先利用CAS更新baseCount的值,如果因为高并发导致CAS更新baseCount失败,那么会new CounterCell(x),x就是addCount(1L, binCount)的参数1L,最后将new CounterCell(x)的对象保存在counterCells数组中,并且counterCells数组的容量一定一个2的幂次数,默认为2,也就是说counterCells数组中CounterCell对象的属性永远是1,那么显而易见的可以看出,ConcurrentHashMap的大小应该是baseCounth和counterCells数组中CounterCell的value值的总和。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
/**
* 如果counterCells数组不为null或者CAS失败都会对counterCells数组做更改。
*/
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))) {
/**
* 操作counterCells数组。
*/
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
/**
* 调用sumCount()统计容量大小,主要用于下边的逻辑,是否达到了扩容阈值。
*/
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
/**
* 如果达到了扩容阈值,且table不为null,且table的长度没有超过最大容量,
* 那么就进入扩容的逻辑。
* 如果是第一个线程走到这里,sc = sizeCtl为扩容阈值。
*/
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
/**
* 可以理解为当前数组的一个标致。因为我们的数组一旦创建后,数组的长度n就是确定的,
* 每次调用这个方法,返回的就是固定大小的数字。resizeStamp的注释这样解释的:
* 返回用于调整大小为n的表大小的戳记位,这个值如果被RESIZE_STAMP_SHIFT左移的
* 话,就会返回一个负数。
* eg:如果n=16,那么rs=32795。
*/
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);
}
/**
* 如果两个线程同时进入while,第一个线程,那么sc>0,会先执行这里,
* rs << RESIZE_STAMP_SHIFT后是一个很小的负数,加2之后也是一个负数,
* 并将sc的值更改为这个负数,那么第二个线程,就会执行sc<0的逻辑。如果两个线程
* 同时执行到此处,那么也只能有一个线程执行这段代码成功。
*/
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
/**
* 真正的扩容逻辑和数据转移逻辑在这个函数中完成。
*/
transfer(tab, null);
s = sumCount();
}
}
}
8. sumCount()
sumCount()是size()中调用的方法,通过counterCells数组的中CounterCell的value值和baseCount相加就能得到整个ConcurrentHashMap的大小。
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
/**
* baseCount为整个ConcurrentHashMap容量的基础。
*/
long sum = baseCount;
if (as != null) {
/**
* 循环counterCells数组,获取CounterCell对象的vaue值,加上sum就是整个
* ConcurrentHashMap的容量。
*/
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
9. transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)
ConcurrentHashMap的数据转移逻辑实现。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
/**
* 实际上,ConcurrentHashMap扩容的时候,可以并发进行扩容操作。每个线程负责一个步长
* (stride)的数据的转移,这个步长是根据当前CPU的个数和数组容量的大小计算的。默认值16。
*/
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
/**
* 步长(stride)默认值16。
*/
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
/**
* 这里完成数组的扩容操作new Node<?,?>[n << 1],实际上就是将n扩大2倍,
* new了一个Node数组。nextTab就是扩容后的新的数组。
*/
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就是全局新数组的引用。
* transferIndex就是旧数组的长度;
*/
nextTable = nextTab;
transferIndex = n;
}
/**
* nextn就是新数组的长度;
*/
int nextn = nextTab.length;
/**
* ForwardingNode对象就是一个标志,里面有一个MOVE标记,值为-1。
* 1、当旧数组上的一个元素为null或者被成功的转移到新的数据上的时候,旧数组上的这个位置就
* 被标记为fwd。当另外一个线程给这个位置上的put元素时,发现这个位置被标记为fwd
* (putVal第1022行逻辑),那么就说明正在有线程进行数据迁移,那么这个线程会帮助进行数据迁
* 移工作。
*/
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
/**
* advance代表当前这个线程是否继续前进,进行下一个元素的数据迁移工作。
* finishing代表当前这个线程参与的本次数据迁移工作的是否全部完成,就是说,当它转移自己所
* 负责的步长的数据的迁移工作后,发现其他的位置的元素都有对应的线程在工作,整个迁移工作已经
* 没有自己的位置了,那么这个标志就为true,这个线程也就完成了数据迁移的使命。
*/
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
/**
* i和bound其实就是步长范围内的起始索引。
* i是步长范围内的最大索引,bound是最小索引。
* eg:如果步长为2,那么i有可能是3/1,bound是2/0。
*/
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
/**
* 如果一开始--i>=bound,或者finishing=true,就直接设置dvance = false
*/
if (--i >= bound || finishing)
advance = false;
/**
* 下边的CAS在不断的更改transferIndex的值,当发现transferIndex<=0,说明步
* 长stride和旧数组长度nextIndex相等。
*/
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
/**
* 如果第一个线程进来,那么会先自行这部分代码:
* eg:步长stride = 2,旧数组长度nextIndex = 4。那么计算出来的nextBound
* = 2。
* bound = nextBound = 2;
* i = nextIndex - 1 = 3;
* 就代表当前线程负责转移2~3这两个位置上的元素。
* advance = false;已经计算出来负责的元素的索引了,那个无需在while了。
*/
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
/**
* 下边的CAS在不断的更改transferIndex的值,当发现transferIndex<=0,说明步长
* stride和旧数组长度nextIndex相等。
*/
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
}
}
/**
* 如果第i个位置上的元素为null,设置当前位置标记为fwd。
*/
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
/**
* 如果第i个位置被标记为fwd,说明这个位置上的元素已经被转移过了。
* 设置advance = true;继续计算下一个步长范围。
*/
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
/**
* 如果第i个位置上的元素是一个链表,给链表加锁,
* 如果第i个位置上的元素是一个红黑树,给红黑树加锁,
* 如果第i个位置上的只有一个元素,那么就给这个元素加锁,
* 不管怎么样,都是第i个位置上的元素。
*/
synchronized (f) {
/**
* 再次判断第i个位置上的元素是否有改变。
*/
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
/**
* 如果第i个位置上的元素是一个链表或者只有一个元素的时候
*/
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
/**
* 这个循环实际上是在找lastRun这个标记的node节点。转移一个链表数据
* 的时候,除了循环链表上每一个元素之外,可以找与第i个节点hash值不同
* 的节点(lastRun),找到这个节点之后,就认为,lastRun之后的所有
* 的节点都应该放在新数组同样的位置(这些节点组成一个新的链表,这个新
* 的链表应该放在新数组第i个位置上或者第i+n的位置上),这是作者在这
* 里提供的一种思想。
*/
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
/**
* runBit可以理解为高位上的值,要么为0要么为1,为0的元素,在新数组
* 的第i个位置上,为1的元素在新数组的第i+n的位置上。
*/
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
/**
* 循环f到lastRun节点之间的元素,给这些节点分类,分为两个链表:
* 低位链表ln:表明上边的所有的元素的高位都是0,这个链表整体应该放在
* i的位置上。高位链表hn:表明上边的所有的元素的高位都是1,这个链表
* 整体应该放在i+n的位置上。
*/
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)
/**
* 这里就是在组装所有高位都是0的元素链表,让当前正在new的
* 节点的next指向当前的ln。那么新的链表就会倒置,但是元素
* 都是正确的。
* eg: 原来的元素排列:ln = node1->node2->lastRun
* 新的的元素排列:ln = node2->node1->lastRun
*/
ln = new Node<K,V>(ph, pk, pv, ln);
else
/**
* 这里就是在组装所有高位都是1的元素链表,让当前正在new的
* 节点的next指向当前的hn。
* 那么新的链表就会倒置,但是元素都是正确的。
* eg: 原来的元素排列:hn = node3->node4->lastRun
* 新的的元素排列:hn = node4->node3->lastRun
*/
hn = new Node<K,V>(ph, pk, pv, hn);
}
/**
* ln在新数组的第i个位置上,hn在新数组的第i+n的位置上。
* 旧数组第i个位置标记为fwd。
* advance = true;当前线程继续找下一个步长。
*/
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
/**
* 如果第i个位置上的元素是一个红黑树
*/
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;
/**
* 如果数的第一个节点不为null,那么从第一个节点开始循环整棵树。
*/
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
/**
* TreeNode实际上也是一个双向链表(prev和next)。先将第i个位
* 置上的红黑树转化为一个双向链表,再根据双向链表重新处理红黑树
* 。
*/
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
/**
* 这个节点的高位为0
*/
if ((h & n) == 0) {
/**
* 根据这个节点new的双向链表节点p,loTail在没有重新赋值
* 时,就代表的是上一个双向链表的节点,重新赋值之后,就代表
* 的是当前双向链表的节点:
* 第一次进来,双向链表的上一个节点(loTail)为null,那么
* 就将当前节点存储在lo(lo = p),并且重新设置loTail =
* p,并且++lc。
* 第二次进来,双向链表的上一个节点(loTail)不为null,那
* 么就将当前节点存出在上一个节点的next,并且重新设置
* loTail = p,并且++lc。lc代表的是高位为0的元素的个数,
* 主要用来判断是否需要由红黑树转化为链表。
*/
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
/**
* 这个节点的高位为1
*/
else {
/**
* 根据这个节点new的双向链表节点p,hiTail在没有重新赋值
* 时,就代表的是上一个双向链表的节点,
* 重新赋值之后,就代表的是当前双向链表的节点:
* 第一次进来,双向链表的上一个节点(hiTail)为null,那么
* 就将当前节点存储在hi(hi = p),
* 并且重新设置hiTail = p,并且++hc。
* 第二次进来,双向链表的上一个节点(hiTail)不为null,那
* 么就将当前节点存出在上一个节点的next,
* 并且重新设置hiTail = p,并且++hc。
* hc代表的是高位为1的元素的个数,主要用来判断是否需要由红
* 黑树转化为链表。
*/
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
/**
* 根据lc判断,低位上的红黑树是否需要转化为链表。
*/
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
/**
* 根据hc判断,高位位上的红黑树是否需要转化为链表。
*/
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
/**
* ln在新数组的第i个位置上,hn在新数组的第i+n的位置上。
* 旧数组第i个位置标记为fwd。
* advance = true;当前线程继续找下一个步长。
*/
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
总结
1、JDK1.8中的ConcurrentHashMap底层数据结构以及数据存储原理。
- 数据结构:数组+链表+红黑树。
- 数据存储原理:实际上数组上第i个位置上存储的是Node对象,当数组上第i个位置上发生hash碰撞的时候,从链表的第一个节点开始比对,寻找存放该Node对象的位置,然后将上一个Node对象的next属性指向该Node对象,这样就组成了一个单向链表,采用的是尾插法的方式。当链表的长度达到8的时候,数组上第i个位置上就存储的是一个TreeBin对象(将链表转化为红黑树),也就是红黑树,TreeBin对象中的TreeNode对象就是红黑树的一个节点,实际上也是一个双向链表,当数组上第i个位置上发生hash碰撞的时候,那么就从红黑树的第一个节点开始比对,寻找存放该TreeNode对象的位置。
2、JDK1.8中的ConcurrentHashMap在什么情况下会将链表转化为红黑树?
实际上,数组上第i个位置上的链表长度超过8的时候,就开始准备将该位置上的链表转化为红黑树,那么最终是否真正的要转化为红黑树,取决于数组的长度是否大于等于64。因为由链表转化为红黑树的目的也就是为了提高插入和查询效率,当链表长度小于8且数组的长度小于等于64的时候,同样可以通过扩容的办法来剪短链表的长度达到同样的效果。也就是说,当链表长度大于等于8且数组的长度大于64的时候才会去将链表转化为红黑树。
3、JDK1.8中的ConcurrentHashMap的链表转化为红黑树的阈值是8?
理想情况下,随机hash算法都遵循泊松分布,可以看出,数组中一个桶上的链表的长度达到8的概率为0.00000006,几乎是不可能达到的,所以当链表长度达到7的时候,就向红黑树转化,实际上,生活中使用到红黑树的概率也是很少的。所以8是根据统计得到的一个最佳的数字。
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
4、JDK1.8中的ConcurrentHashMap为什么要使用红黑树?
如果数组中一个桶中链表的长度为8,红黑树的平均查找长度是log(n),平均查找长度为log(8)=3,链表的平均查找长度为n/2,平均查找长度为8/2=4。这时候,使用继续使用链表的话,查询元素的效率就会很慢很慢,然而转化为红黑树的话,查询效率就会提升。
5、JDK1.8中的ConcurrentHashMap中put的流程。
- ConcurrentHashMap中不允许null key和null value。首先会判断,如果是null key和null value则会抛出NullPointerException。
- 根据key计算hash值,如果表为null,则会去创建表initTable,然后计算key存放的索引位置i = (n - 1) & hash)。
- 如果索引i位置已经存在元素,不管是链表还是红黑树,都是从第一个节点开始,不断的进行循环查找当前元素的存放位置:
- 如果找到key相同的元素,然后用新的value替换旧的value,并将旧值返回。
- 如果索引i位置是链表,那么就采用尾插法将该元素插入到链表的尾部。
- 如果索引i位置是红黑树,找到该元素的位置插入节点后,进行红黑树的平衡调整,变色,左旋或者右旋。
- 如果索引i不存在元素,则将key-value放在该位置上。
- 如果索引i位置已经存在元素,不管是链表还是红黑树,都是从第一个节点开始,不断的进行循环查找当前元素的存放位置:
- 树化判断,当链表的长度大于8,且数组的大小超过64的时候,就会将链表转化为红黑树。
- 统计元素的数量(baseCount+counterCells),判断是否需要扩容。当数组的长度超过数组大小*0.75(扩容因子)时,就会触发扩容操作。
- 扩容时,新数组的容量大小为旧数组容量大小的2倍(n<<1)。
- 新数组创建后,需要将旧数组的数据迁移至新数组,一个线程默认负责数据的16(默认步长)个桶的位置的数据迁移工作。
- 迁移后,红黑树可能会被拆分为多个红黑树,或者多个链表。较大的链表可能会被拆分多个较小的链表。
- 因为限定了数组初始化时的容量是2的n次幂,那么迁移数据时,只需要看n位置上的二进制位是0或者1即可,如果是0,则旧数组上的位置就是新数组上的位置。也就是新旧数组存放改元素的索引是一致的,但是如果是1,则在旧数组的的索引基础上增加原来的数组的长度就可以确定该元素在新数组上的索引位置。实际上,索引位置是相对的。如果旧数组长度为16,元素在旧数组中索引是2,那么在新数组上只有两种情况,2或者18(2+16),源码中transfer()方法第2454行和2455行有所体现:
/**
* nextTab:就是新数组
* i:就是元素在旧数组上的索引
* n:就是旧数组的长度
*/
int n = tab.length, stride;
for (int i = 0, bound = 0;;) {………………}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
6、红黑树的特点:
- 每个节点或是红色的或是黑色的。
- 根节点是黑色的。
- 每个叶子节点是黑色的。
- 如果一个节点是红色的,那么它的两个儿子节点是黑色的。
- 对每个节点,从该节点到其子孙节点上的所有路劲上的黑色节点数目相同。
- 待插入的节点默认是红色的。