Striped64
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
如果在多线程中原子的修改一个变量。 原子类使用的是自旋锁机制来保证修改成功的。
比如在一个性能比较一般的机器上,开启20条线程使用原子类累加一个数据。机器性能一般
所以cpu修改一个数据有点慢,结果就是 一个线程在修改数据,修改期间,其他19个线程
在空转。原本机器性能就不好,现在更是雪上加霜。 都还不如使用同步代码块的方式累加,
至少其他19个线程不去抢占cpu。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
如果对一个资源竞争太过激烈,使用自旋锁的方式反而会拖累系统。
所以这个类就出现了。解决的问题是,把激烈的竞争进行分块,就好像是,2车道太拥挤,
那就扩路,改成8车道。每个车道就没有那么拥挤了。
竞争程度一旦降低了,自旋锁的优势就出来了。原来需要自旋100次才能设置成功的,
现在基本1,2次就行,那不要太爽了。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这个类中的几个重要变量:
//cpu核数
static final int NCPU = Runtime.getRuntime().availableProcessors();
/*
* 不是要把竞争分块吗? 这个就是一个个的块,每个线程分配到某个块中。
* 各个线程在块中计算,把结果保存到各自的块中,后续从块中取出各块结果,再次计算
* 这个变量是一个数组,会在程序的执行中根据竞争的激烈程度扩容。但是数组的长度
* 必须是 2 的幂次。 而且长度是大于或者等于 CPU 核数 最小的 2 的幂次
* 比如 cpu 核数是 7, 长度最长 8
* 比如 cpu 核数是 8, 长度最长 8
* 比如 cpu 核数是 9, 长度最长 16
*/
transient volatile Cell[] cells;
/*
* 如果竞争不激烈,那么就没必要分块了, 那么 base 就是不分块,数据累加的地方。
* 或者一开始竞争不激烈,后面就激烈了,那么不激烈的时候有一部分数据已经累加到这里了
* 还有一部分:正在分块中, 块还没有完全分好,线程又提交了数据,还是要累加到这里。
*/
transient volatile long base;
/*
* 本类自己的自旋锁。 用于自己同步代码块的。 后面再说
*/
transient volatile int cellsBusy;
private static final sun.misc.Unsafe UNSAFE; //unsafe
private static final long BASE; //获取变量 base 的偏移地址的
private static final long CELLSBUSY; //获取 cellsBusy 偏移地址的
private static final long PROBE; //获取 Thread 类中一个变量的偏移地址
Thread 类中为了本类的实现,添加了一个 int 类型变量 threadLocalRandomProbe
PROBE 就是 找到 threadLocalRandomProbe 变量相对于 Thread 的偏移地址的。
以后通过 Thread.currentThread() 获取到当前线程,然后再使用这个偏移量就能够知道
当前线程这个变量的内存地址了,然后通过内存地址的方式原子的修改他的值了。
Thread中的这个变量的值 是多少不重要,文档上说是一个伪随机数字。
//还有一个内部类,就是分块的 Cell
@sun.misc.Contended static final class Cell
{
//分块累积数据就保存再这里
volatile long value;
//构造
Cell(long x) { value = x; }
//提供了一个对自己 value 内存原子操作的方法
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
..... 其他的 unsafe, value 的偏移量啊
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
它的简单介绍就完了,它是一个抽象类,不能直接使用, 所以使用他的子类,
下面介绍他的子类, 然后会回看本类中两个重要的方法。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
LongAdder
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
从名字就直接看出来它的功能,long 类型的累加器。
类的定义:继承了前面说的 Striped64
public class LongAdder extends Striped64 implements Serializable {}
只有唯一的无参构造方法
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
先使用它:
开启40个线程,每个线程执行 1 亿次累加 1, 线程结束,输出结果
private static void t1_striped_1() throws Exception {
int count = 40;
CountDownLatch countDownLatch = new CountDownLatch(count);
LongAdder longAdder = new LongAdder();
for (int i=0; i<count; ++i) {
new Thread(() -> {
for (int k = 0; k < 100000000; ++k) {
longAdder.add(1);
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(longAdder.longValue());
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
线程结束,获取结果: longAdder.longValue()
public long longValue() {
return sum();
}
public long sum() {
Cell[] as = cells; Cell a;
//线程刚刚开启的时候,第一个线程执行特别顺利,没有竞争,
//所以,就有可能把数据累加到 Striped64 的 base 上了
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
//遍历各个块,把各个块中累积的数据统计到一起
sum += a.value;
}
}
// base + 各个块的累积数据 就是最终的数据了。
return sum;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
累加数据: longAdder.add(1);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
/*
* 这个 if 中有两个判断
* 1、当前还没有块,即块是空,
* 执行第2个判断, 把数据累加到 base 上是否能够成功,
* 成功:竞争不激烈,能够一次性设置成功, if 进不去
* 失败:说明有竞争,if可以进去了
* 2、已经有块了,有块了说明竞争存在了,以后这个 if 都能进来
* 以后的数据也不要累加到 base 上了,要累加到各个块中
*/
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
/*
* 执行到这里,说明竞争是存在的。分块是需要的。
*
* 这个 if 要怎么才能进去:
* 1、块是空
* 2、块不空,但是长度是0, 相当于 空
* 3、当前线程分配的块还没有赋值,还是相当于空(这是有可能的,比如正在给这个
* 数组元素赋值时--初始化块,另一个线程执行到这里就会出现)
* 4、在分配的这个块的中还是累加失败,说明块中又有竞争, 在弄多一点的块?
*/
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended); //if 进入就执行这个重要的方法
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
//获取当前线程的 threadLocalRandomProbe 的值,如果还没有初始化,就是 0,
//以后简称这个值是 probe, 名字太长了
int h;
if ((h = getProbe()) == 0) {
//这个方法的执行会给当前线程的 probe 使用 unsafe 的方式赋一个随机值
ThreadLocalRandom.current(); // force initialization
h = getProbe(); // 获取这个随机值
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
//---------------------------------------------------------------------1
if ((as = cells) != null && (n = as.length) > 0) {
//-------------------------------------------------1.1
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
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;
}
//-------------------------------------------------1.2
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//-------------------------------------------------1.3
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
//-------------------------------------------------1.4
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
//-------------------------------------------------1.5
else if (!collide)
collide = true;
//-------------------------------------------------1.6
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
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
}
//-------------------------------------------------1.7
h = advanceProbe(h);
}
//---------------------------------------------------------------------2
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
//---------------------------------------------------------------------3
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
---------------------------------------------------------------------2
如果一个线程第一次进入本方法,那么就会进入 这个 else if
因为当前的块还没有建立,所以 ----1 的 if 不成立
cellsBusy 也还是 0, 处于空闲状态
cells 和 as 也都还是 null,
casCellsBusy() 把空闲状态原子的修改成正忙状态,也会成功
一旦进入这个方法,说明状态改成正忙(即 cellsBusy = 1)
进来后创建一个长度为2个块,然后,当前线程在2个块中选一个,把本次需要累加的值 X
累加到块中。在 finally 中把状态改成空闲。 也就是说, 在 casCellsBusy() 把状态改成1
到 finally 中状态改成 0 这中间的代码只能一个线程在执行, 即一个线程独占式执行,
其他线程执行到这里时,无法执行这里的代码,只能跳过这里,继续下面的执行
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
---------------------------------------------------------------------3
如果第一个线程进入 -----2, 在创建2个块时, 还有一个线程进来这个方法。
当前块还没有创建好,所以来到 ------2 的判断中 cellsBusy == 0 和 cells == as
都成立,但是 casCellsBusy() 会失败,因为第一个线程已经改过了,所以
第二个线程只能来到 -----3, 把结果继续累加到 base 上。如果成功就 break 退出了
如果不成功,那就继续循环(这次累加不能丢啊),再次循环,要么块创建好,进入 ----1
要么块还是没有创建好 继续来到 -----3, 再次累加到 base 上。
所以 -------2 只能进来一次,------3 是 当块还在创建过程中,其他线程的唯一的选择。
一旦 -------2 中的块创建好了, -----2 和 -----3 都进不来了, 以后都会进入 -----1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
在介绍 ------1 之前先来说说一个线程会分配到哪个块中。
m = cells.length - 1;
h = getProbe()
那么这个线程对应的块的位置就是 m & h
cells 的长度是 2 的幂次方, 然后减1 得到的数 二进制 低位是连续的 1
比如31,二进制是 0001 1111, 15 二进制 0000 1111
和这样的数进行 与运算,得到必然是小于等于这个的数,最小为0,所以不会数组越界。
比如一个线程的 probe 和 2^N-1 与运算 得到 3, 那么这个线程以后永远都是放到
下标为3的块吗?也不是, -------1 代码块最后一行代码 h = advanceProbe(h);
所以 每次循环 线程中的 probe 是要改变的。即一个线程的累加一会累加到 块2
一会累加到块 7, 这都不一定。这也很好理解,比如一个线程在某个块中累加数据失败,
说明这个块存在竞争,下次循环如果不换一个块,继续来到这个块,大概率的还是竞争。
此时其他的块可能已经空闲了造成浪费, 所以,同一个线程会在不同的块中碰运气
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
---------------------------------------------------------------------1
这里的代码:是当块已经创建,然后就只能进入这里执行了。
这里有 if - else if 6 个分支。
-------------------------------------------------1.1
//当前线程分配的的块还没有赋值
if ((a = as[(n - 1) & h]) == null) {
//需要当前状态是空闲状态,因为下面的代码需要一个线程独占的执行。看谁能抢到了
if (cellsBusy == 0) { // Try to attach new Cell
//创建块对象,把要累加的值 x 作为初始值给块
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) { //把空闲改成正忙,又是独占代码块
boolean created = false;
try { // Recheck under lock
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; //-----------------------------------独占解除
}
// 本线程执行块赋值(执行了独占代码),create 才为 true,
// 累加值已经放到块中,作为新创建的块的初始值,任务完成,可以退出循环了
if (created)
break;
//执行到这说明,当前线程不是执行独占代码的线程,其他线程独占上面那个代码块
//所以本线程只能来到这里,继续下一次循环,
//下次循环希望块赋值完成了,否则又会来到这里(因为 probe 没有更新)
continue; // Slot is now non-empty
}
}
collide = false;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//-------------------------------------------------1.2
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//-------------------------------------------------1.3
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
-----1.2 什么时候满足: 首先 -----1.1 不满足时, 即自己的块已经被赋过值。
属于自己的块已经被赋值,那么自己只能在这个块中完成累加工作。所以如果
当前 wasUncontended 是 false 也要改成 true。
wasUncontended 为 true 表示当前没有竞争, 来到 ----1.2 时说明自己的块已经赋值了,
即使有竞争也要在这个块中完成累加,所以有竞争也要看成没有竞争。---1.2 出来后,改变
probe 的值,接着下一次循环。 下一次循环可能又换了块, 如果换的块已经完成了初始化,
那么 -----1.2 已经进不来了。
既然 -----1.2 进不去,那么试一下 ------1.3 在这个块中尝试累加。
如果成功就退出循环,如果不成功,改变probe的值,很可能又换一个块,
在这个块中再碰碰运气。 或者怀疑一下,块够吗??需要更多的块来进一步的减缓竞争?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//-------------------------------------------------1.4
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
//-------------------------------------------------1.5
else if (!collide)
collide = true;
//-------------------------------------------------1.6
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
/*
* 扩容为当前长度的2被,需要是2的幂次方法
* 为什么非要是2的幂次方。2^N-1 的二进制是前面是连续的0,后面是连续的1
* 比如长度是13, 二进制是 0000 1101 , 后面的1不连续,造成与运算的结果
* 出现 0 的概率偏大。
* 所以 2^N-1 的特性,即能保证与运算的结果不超过这个数字,还能保证概率均衡
*/
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
}
-------1.4:
当前块的数量是不是大于cpu核数了, 或者 cells != as
看看这的老大 ------1 中的代码 if ((as = cells) != null && (n = as.length) > 0) {...}
as 是 cells 的赋值, 来到 ----1.4 居然还要判断 cells != as, 而且还真的可能不等。
这是因为这里是多线程在执行,cells 是成员变量, as 只是局部变量。
as 是 cells 的赋值,最后 cells != as 只能说其他线程修改了 cells 的值。
比如 自己的线程 cells 给 as 赋值为 100, 其他线程把 cells 改成 200,
此时自己的线程运行到这里再次判断时就不等了。
所以 -----1.4 如果 cells != as 只能是其他线程修改了 cells 的值,修改它的值
只能是为了给 cells 扩容。 无论是扩容,还是块的容量已经达到最大值了,那么都认为
如果有冲突,那么冲突已经解决(已经做了所能做的事,还要怎么样)。 即 collide = false;
------1.5 前面 if -- else if 都没有进去(---1.6中详细说明),collide 的值都是false
进入这里改成 true, 这里是唯一把 collide 改成 true 的地方。然后下一次循环
------1.6 进入条件是: 所在的块已经赋过值 ---1.1 ,而且现在也没有竞争 ----1.2,
但是自己在块中设置值仍然失败 ----1.3 , 块还没有达到最大值,而且也没有人去扩容 ---1.4
collide 的值是 true, 因为上次循环进去 ----1.5 改成true, 然后其他条件不变,才能进入这里
进入 ----1.6 就是先设置独占代码块, 然后扩容, 修改了 cells 的值,然后下一次循环
后续的运行,新扩容的块又是 空,进入 ---1.1 .......
一旦扩容达到最大值。-------1.4 和 ------1.5 便形成了一个对 -------1.6 的保护屏障。
一旦扩容达到最大,且每个块都被赋值(很容易达到,因为块也就那么几十个,和cpu有关)
然后大量的工作是 在 ---1.3 ---1.4 ---1.5 中运行。collide 变量的值在 -------1.4,
------1.5 中每次循环改一次,让 ----1.6 不再有机会执行, 而他们的出路就是在 ---1.3
中累加成功 然后 break 。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
经过上面分析,LongAdder 在多线程中通过设置多个块, 把累加任务分配到各个块中累加
从而降低线程的竞争,提高自旋锁的命中率。然后再把每个块中的数据 和 base 累加得到
最终的结果。