JUC--003--Atomic-3

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 累加得到
最终的结果。



 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值