数据结构
1.8跟1.7不一样了,1.7由segment+HashEntry组成,1.8是由数组+链表+红黑树,倒是跟1.8的HashMap有点像
问题
1.7是通过segment分段锁实现线程安全的,1.8没有了segment是怎么保证线程安全的
数组大小初始化
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
// initialCapacity + (initialCapacity >>> 1) + 1
//这个传入就会比 initialCapacity 大很多,不止二的幂次方了
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
//下面这个方法 跟HashMap 1.8一样
private static final int tableSizeFor(int c) {
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
initTable
数组的初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//当数组为空时候进行死循环
while ((tab = table) == null || tab.length == 0) {
//1.sizeCtl没有初始化,所以为零
//2.sizeCtl用volatile修饰了,保障了可见性
//3.sizeCtl 是什么?相当于阈值
if ((sc = sizeCtl) < 0)
//如果 sizeCtl 小于零说明现在有线程在执行初始化数组
//就展示放弃该线程cpu执行权
Thread.yield(); // lost initialization race; just spin
//如果sizeCtl不小于零,通过cas修改 sizeCtl 的值 为 -1,表示
//现在有线程在执行数组初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//cas成功之后再次判断 数组是否为空,为空才进行初始化
if ((tab = table) == null || tab.length == 0) {
//默认是16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//初始化数组容量
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// n - (n >>> 2) 可以看作n减去(n除以四分之一)等于四分之三n
//不就是等于1.7的加载因子0.75
//sc 等于0.75n,也就是阈值
sc = n - (n >>> 2);
}
} finally {
//sizeCtl 就是阈值
sizeCtl = sc;
}
break;
}
}
return tab;
}
initTable 总结:
- 1.
initTable怎么保证线程安全的?通过sizeCtl来控制的 - 2.当有线程过来会先判断
sizeCtl是否小于零,如果小于零就放弃cpu执行权 - 3.如果
sizeCtl不是小于零,也就是初始化0,就用cas将sizeCtl修改为-1 - 4.
cas修改成功之后,会进行数组的初始化 - 5.
1.8的阈值的计算跟之前不一样,之前直接是数组大小*0.75,1.8是通过n-(n >>> 2)来计算的,可能作者觉得这样效率更高还是咋地了 - 6.所以数组初始化是通过
cas控制sizeCtl来保证线程安全的
putVal
final V putVal(K key, V value, boolean onlyIfAbsent) {
//跟1.7一样,key不能为空
if (key == null || value == null) throw new NullPointerException();
//1.8的取hash比较简单,就是取原来的hash,然后用高位扰动一下
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();
//定位出数组位置,如果数组位置为空,通过`U.getObjectVolatile`来获取数组位置上值
//就通过`U.getObjectVolatile`来设置数组大小
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//U.compareAndSwapObject cas设置数组
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果头节点的 hash=-1,表示现在节点正在扩容,会通过 helpTransfer 帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//对头节点 f 加锁
synchronized (f) {
//tabAt 获取内存可见的数组,再次判断头节点是否等于 f
if (tabAt(tab, i) == f) {
//fh >= 0 表示没有在进行扩容
if (fh >= 0) {
binCount = 1;
//遍历整个链表,binCount 记录遍历的次数
//binCount = 原链表长度
for (Node<K,V> 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<K,V> pred = e;
//遍历链表知道最后一个,采用尾插法
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//如果是红黑树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//插入红黑树
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//转链表的条件 binCount >= 8 也就是说链表原有八个,新增一个
//此时就可以转红黑树,这个跟1.8的 HashMap 一样只是 binCount 的获取方式不一样
if (binCount >= TREEIFY_THRESHOLD)
//树化
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//增加容器长度
addCount(1L, binCount);
return null;
}
putVal 总结:
- 1.
putVal会先去判断数据是否未初始化,如果没有初始化就初始化容器initTable - 2.如果定位到的数组的头节点为空,就通过
cas设置数组位置上的头节点 - 3.如果数组头节点不为空,会通过头节点的
hash是否等于-1来判断是否在扩容,如果在扩容就调用helpTransfer来帮助扩容 - 4.以上都不满足就会取正在的添加节点,为了保证添加节点的过程线程安全,会对头节点这个对象进行加锁
- 5.加锁之后有两个分支,一个是链表,一个是红黑树
- 6.如果是链表就遍历链表,先判断是否有
key相同,如果相同就覆盖value,如果不同就尾插,同时记录下原链表长度 - 7.如果是红黑树就走
putTreeVal去添加节点 - 8.最后会通过链表长度来判断是否需要链表转化为树,条件是链表的长度大于等于8,这个跟
1.8的HashMap一样 - 9.以上都执行完如果有新增节点,就会调用
addCount去往容器大小加一 - 10.需要注意点是在往数组添加头节点是通过
cas来保证线程安全,在往链表和红黑树添加节点的时候是通过在头节点上加synchronized锁,有别于1.7的ConcurrentHashMap通过ReentrantLock来锁住segment,用的锁和锁的对象都不一样。 - 11.这个方法还差帮助扩容:
helpTransfer,插入树节点:putTreeVal,树化:treeifyBin,统计长度:addCount
putTreeVal 插入树节点
需要注意的是1.8的ConcurrentHashMap跟1.8的HashMap不一样的是:ConcurrentHaShMap
增加了TreeBin,TreeBin会记录树的头节点,然后数组存放的元素也是TreeBin为什么要加TreeBin这个类,因为我们加锁一般是对头节点进行加锁,每次往树加入节点都有可能进行树平衡,导致树的头节点变化,这个加锁的对象可能就错了,而引入TreeBin这个对象放到数组中,每次从数据获取到的对象都是他,不会因为树的平衡而导致树头节点变化。
//继承了 Node
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;
}
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
//如果头节点是空,新增的节点就是头节点也是链表头节点
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
//跟1.8的hashmap一样,通过各种比较判断新增节点是左节点还是右节点
else if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
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;
}
dir = tieBreakOrder(k, pk);
}
//找到要插入的位置,开始插入新节点
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//设置链表的头节点为新增的节点
//红黑树的链表采用的是头插法
TreeNode<K,V> x, f = first;
first = x = new TreeNode<K,V>(h, k, v, f, xp);
if (f != null)
//双向链表
f.prev = x;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//如果父节点是黑色,就直接将新增节点置为红色,不需要去平衡树
//因为此时是满足红黑树的各种条件
if (!xp.red)
x.red = true;
else {
//父节点是红色,需要进行树的平衡
//lockRoot 去通过cas将 lockState
//锁标志从0(表示没有线程在进行平衡树)改为1(表示有线程在平衡树)
//如果cas成功就进行平衡树,如果cas失败就park线程
lockRoot();
try {
//跟1.8的hashmap一样,不看
root = balanceInsertion(root, x);
} finally {
//直接 lockState = 0 去释放锁
unlockRoot();
}
}
break;
}
}
assert checkInvariants(root);
return null;
}
putTreeVal 总结:
- 1.插入树的逻辑跟
1.8的HashMap大体类似,不一样的有两点 - 2.第一点增加了
TreeBin来保证加锁过程中节点的不变 - 3.第二点是在平衡树的时候
balanceInsertion,会通过cas修改lockState值来加锁,保证balanceInsertion平衡树的线程安全
treeifyBin 树化
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//准备树化的时候,如果数组长度小于64就进行扩容
//这点跟1.8的HashMap一样
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
//对头节点进行加锁
synchronized (b) {
//重复判断下,头节点有没有变
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
//遍历单项链表,把单项链表改成树,同时形成双向链表
//这个也跟1.8的Hashmap一样
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;
}
//1.new一个 TreeBin,其实就是去设置树,平衡树然后把头节点放到
//TreeBin 的root属性上,new TreeBin 里面的树化跟1.8的Hashmap一样,不看了
//2.setTabAt,是把 TreeBin 放到数组的位置上
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
treeifyBin 总结
- 1.树化过程还是基本跟
1.8的HashMap一样,不同有两点 - 2.第一点:会对头节点
synchronized加锁 - 3.第二点:数组位置放的不是树的头节点,而是
TreeBin对象,TreeBin对象有头节点属性 - 4.
TreeBin的创建过程跟1.8的HashMap树化一样
tryPresize 扩容
todo transfer
helpTransfer 帮助扩容
todo
addCount 统计大小
ConcurrentHashMap大小是通过两个属性控制的- 一个是
volatile long baseCount - 一个是
volatile CounterCell[] counterCells数组,数组里面的元素CounterCell维护着volatile long value
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//会先去判断 counterCells 数组是否空,如果为空,就通过 cas 往 baseCount 加一
//counterCells 为空 或者 (counterCells不为空而且修改baseCount失败)就走下面的逻辑
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
//有几个情况会走 fullAddCount 去添加容器大小
//1.counterCells 空
//2.通过 & 来定位的 counterCells 为空
//3.通过cas 往 counterCells 定位上的位置 value+1 失败
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;
//完成容器+1 之后进行统计
s = sumCount();
}
//check 其实就是 putVal 的bincount
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//如果 容器大小 s 大于等于阈值 sizeCtl 就进行扩容
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);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
fullAddCount
往容器大小加 x,主要是要往baseCount或者counterCells加x
private final void fullAddCount(long x, boolean wasUncontended) {
//h 是通过 ThreadLocalRandom 为每个线程生成一个随机数
//每个线程都不一样,是用来定位每个线程要操作 `counterCells`
//其实就相当于 hash 用来定位数组的位置一样
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
//最外层的if,如果 counterCells 不为空就走下面的逻辑
if ((as = counterCells) != null && (n = as.length) > 0) {
//下面都是 counterCells 数组不为空的情况
//如果定位到 counterCells 某个坑位 为空 就去初始化
if ((a = as[(n - 1) & h]) == null) {
//判断 cellsBusy 是否等于0 等于0表示现在没有线程在操作 counterCells
if (cellsBusy == 0) { // Try to attach new Cell
//初始化一个 CounterCell 对象
CounterCell r = new CounterCell(x); // Optimistic create
//再去判断 cellsBusy 是否等于0
//然后通过cas 去修改 cellsBusy 的值为1 表示现在有线程在修改 counterCells
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
//如果cas 成功
boolean created = false;
try { // Recheck under lock
//将 CounterCell 元素放到 CounterCells数组里面
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;
}
//如果上面的if失败也就是说定位的坑有已经有值了,就会走下面这些if
//wasUncontended 如果外面有一次cas 去修改 counterCells 失败了
//wasUncontended 就等于false 否则就是true
//如果wasUncontended = false 就修改 wasUncontended = true 然后再次循环
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//如果 wasUncontended=true 就会通过cas 去修改 数组坑位上的值,修改成功就跳出
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
//如果 counterCells 发生变化 或者 counterCells 长度大于等于数组的核心树
//collide = false 表示不进行 counterCells 扩容了
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
//如果 collide = false 就会在这个if里面把 collide 改为 true
//这个一直都走不到下面扩容的逻辑因为上面的if改为false,下面这个
//如果 collide =false就会走这个逻辑
else if (!collide)
collide = true;
//如果可以扩容也就是 collide=true
//先判断 cellsBusy 是否等于0 也就是说没有线程在操作 counterCells
//通过cas 去修改 cellsBusy = 1 表示现在有线程在修改 counterCells
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
//开始对 counterCells 进行扩容 大小是原来的两倍
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
}
//扩容之后,重新通过 ThreadLocalRandom 去获取h相当于重新hash一下
h = ThreadLocalRandom.advanceProbe(h);
}
//如果 counterCells 为空,就走这个最外层的if
//通过cas 将 CELLSBUSY 修改为 1 表示目前有线程正在修改 counterCells
//如果cas CELLSBUSY 成功 就说明没有线程在修改 counterCells,走下面的逻辑
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
//初始化 counterCells 数组 默认大小2
//h & 1 定位到需要修改的位置
//设置 counterCells
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
//最后把 cellsBusy 修改为 0 表示这个线程已经操作完 counterCells
//其他线程可以接着操作
cellsBusy = 0;
}
if (init)
break;
}
//如果上面的两个if 都不成功,可能走到第二个if,有其他线程已经初始化了 counterCells
//就走这个 最外层的if 通过cas 修改 baseCount
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
fullAddCount 总结:
- 1.为了保证往 容器大小加一线程安全
ConcurrentHashMap维护了CounterCells数组和baseCount来保证线程安全 - 2.
baseCount比较简单就是通过cas来修改 - 3.
CountCells比较复杂,要保证这个数组操作的线程安全,主要通过cellsBusy和cas来保证对counterCells操作的线程安全 - 4.每次需要操作
counterCells都会cas cellsBusy,如果cas成功,就会到定位的counterCells位置上去修改value,cas失败会去各种尝试,还会去对counterCells进行扩容,扩容大小是原来数组大两倍,扩容之后会去重新通过ThreadLocalRandom获取h,相当于重新hash为了就是获得新的坑位。
sumCount
统计容器大小
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
//baseCount 加上遍历 counterCells 数组
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
其实就是 baseCount+counterCells上每个坑位的value
未完待续.................