1 前言
上一节我们对LongAdder的底层源码、实现机制进行了深入了剖析,包括AtomicInteger在高并发竞争下导致的大量自旋的问题,以及LongAdder是怎么使用分段锁优化这个问题的。我们最后看到longAccumulate托底的方法,这一节我们来深入的分析一下Striped64的分段锁机制的实现。
2 Striped64分段锁底层实现原理
我们上一节看到LongAdder继承自Striped64,底层分段锁还是基于Striped64d的longAccumulate方法,我们直接接着上一节的内容,继续查看longAccumulate的方法源码:
我们先从整体来看下这个方法内部做了几件事:
longAccumulate里面做了几件事情,先从大角度分析一下:
(1)进入方法,黄色部分,首先就是获取一下用户id,如果为0,先强制初始化一下用户id
(2)然后就是进入for死循环里面,只有用户的请求成功处理了,才会退出循环
(3)然后就是下面比较重要的三个大的分支条件
进入备用窗口处理
// cells是备用窗口列表,如果备用窗口不等于null,
// 并且是length>0 说明备用窗口开启了
// 则用户进入这个条件,去备用窗口列表里面处理
if ((as = cells) != null && (n = as.length) > 0)
初始化备用窗口
如果上面的分支不满足,说明 cells == null 或者 cells.length <= 0 说明备用窗口没开启啊,这个时候就要开启一下备用窗口,即进行备用窗口列表数组的初始化工作:
// cellsBusy 是备用窗口列表是否在初始化或者扩容标志
// cellsBusy == 0 说明尚未有人初始化或者扩容
// 这时候执行到这里的线程,就要进行对备用窗口初始化了
// 比如说线程1走到这里,发现没初始化,就要执行一下casCellBusy告诉别人正在初始化
else if (cellsBusy == 0 && cells == as && casCellsBusy())
竞争常规窗口
如果cellsBusy == 1 说明有人在初始化备用窗口或者对备用窗口列表进行扩容,这个时候备用窗口列表不可用,只能去常规窗口争抢了
// 直接对常规窗口争抢,如果成功就退出死循环了
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
我们画个图再加强理解一下:
(1)比如用户1进入了这里之后,首先判断分支1,发现备用窗口没有启用;然后直接进入分支2,去初始化备用窗口
(2)然后用户2也进来了,发现备用窗口没启用,同时发现有人在初始化备用窗口,直接进入分支3,去争抢常规窗口了
(3)备用窗口初始化好了,开启了之后;用户的请求进来都走到分支1,去备用窗口列表去竞争去了。
2.1 初始化备用窗口
由于备用窗口没有开启,所以最开始有一个线程会进入分支2,我们就先来看看分支2的源码实现:
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
// init是否初始化完成,最开始设置为false
boolean init = false;
try { // Initialize table
// 然后这里就没什么复杂的,就是创建一个数组
// 但是这个时候数组里面没有元素,每个元素都是空的
if (cells == as) {
Cell[] rs = new Cell[2];
// 创建一个新的窗口,把自己的操作数x交给新创建的窗口
rs[h & 1] = new Cell(x);
cells = rs;
// 然后设置初始化完成表示为true
init = true;
}
} finally {
// 重新设置cellsBusy标识,操作完成,没人在初始化或者扩容了
cellsBusy = 0;
}
// 如果初始化完成,退出循环
if (init)
break;
}
如果cells备用窗口时null,则初始化一下;同时如果自己被派遣到的窗口格子是空的,则创建一个窗口格子(相当于叫一个工作人员过来)
2.2 备用窗口已开启,则去备用窗口
if ((as = cells) != null && (n = as.length) > 0) {
// 如果自己被派到的那个备用窗口为null,说明窗口还没人工作,则叫一个工作人员过来负责
if ((a = as[(n - 1) & h]) == null) {
// 如果没人在初始化,下面则创建一个窗口(叫一个工作人员过来处理)
if (cellsBusy == 0) {
// 创建一个新的窗口,同时提交自己的请求x
Cell r = new Cell(x);
// 如果没人在初始化
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try {
Cell[] rs; int m, j;
// 再次检查备用窗口列表初始化好了
if ((rs = cells) != 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;
}
// 如果到这里已经知道cas操作失败了,则重新设置一下失败标识,进入下一循环重试
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 根据 用户id % 备用窗口总数 算法,自己应该争抢哪个备用窗口
// 如果争抢成功,则操作结束了,如果失败进入下一循环重试
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
else if (n >= NCPU || cells != as)
collide = false;
else if (!collide)
collide = true;
// 走到这里,说明上面的操作都失败了,备用窗口竞争还是很激烈
// 所以需要扩容了,多增加一些备用窗口,缓解竞争激烈的情况
else if (cellsBusy == 0 && casCellsBusy()) {
try {
// 这里就是具体的扩容操作了
if (cells == as) { // Expand table unless stale
// 新的备用窗口是原来的2倍
Cell[] rs = new Cell[n << 1];
// 原来已经存在的备用窗口重新赋值一下
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
// 在这里重新计算一下自己的用户id,
// 使得用户id尽量的分散到不同的窗口,减少竞争
h = advanceProbe(h);
}
我们在对上面的核心步骤,截图给再说一下:
3 小结
好了Striped64分段锁的源码我们就看到这里,采用了分治法的思想,一个常规窗口+起始的两个备用窗口,随着竞争的变化可能会开辟多个备用窗口再,增长因子为原来的2倍,有理解不对的地方欢迎指正哈。