juc-atomic原子类之六:Striped64累加器

并发之Striped64(累加器),对于该类的实现思想:

   Striped64是在java8中添加用来支持累加器的并发组件,它可以在并发环境下使用来做某种计数,Striped64的设计思路是在竞争激烈的时候尽量分散竞争,在实现上,Striped64维护了一个base Count和一个Cell数组,计数线程会首先试图更新base变量,如果成功则退出计数,否则会认为当前竞争是很激烈的,那么就会通过Cell数组来分散计数,Striped64根据线程来计算哈希,然后将不同的线程分散到不同的Cell数组的index上,然后这个线程的计数内容就会保存在该Cell的位置上面,基于这种设计,最后的总计数需要结合base以及散落在Cell数组中的计数内容。这种设计思路类似于java7的ConcurrentHashMap实现,也就是所谓的分段锁算法,ConcurrentHashMap会将记录根据key的hashCode来分散到不同的segment上,线程想要操作某个记录只需要锁住这个记录对应着的segment就可以了,而其他segment并不会被锁住,其他线程任然可以去操作其他的segment,这样就显著提高了并发度,虽然如此,java8中的ConcurrentHashMap实现已经抛弃了java7中分段锁的设计,而采用更为轻量级的CAS来协调并发,效率更佳。
    为了更加清楚的明确我上面的阐述,请看下面的图示:

 源码:

   /**
     * cell表,容量为2的次幂
     */
    transient volatile Cell[] cells;
 
    /**
     * 基础值,在更新操作时基于CAS无锁技术实现原子更新
     */
    transient volatile long base;
 
    /**
     * 自旋锁  用于保护创建元素或者扩展Cell数组时。 
     */
    transient volatile int cellsBusy;

成员变量cellsBusy

cellsBusy,它有两个值0 或1,它的作用是当要修改cells数组时加锁,防止多线程同时修改cells数组,0为无锁,1为加锁,加锁的状况有三种 
1. cells数组初始化的时候; 
2. cells数组扩容的时候; 
3. 如果cells数组中某个元素为null,给这个位置创建新的Cell对象的时候;

成员变量base

它有两个作用: 
1. 在开始没有竞争的情况下,将累加值累加到base 
2. 在cells初始化的过程中,cells不可用,这时会尝试将值累加到base上;
让我们来看看Cell这个类;它被Contended修饰,目的是为了防止变量的伪共享;

成员变量cells

cells数组是LongAdder高性能实现的必杀器: 
AtomicInteger只有一个value,所有线程累加都要通过cas竞争value这一个变量,高并发下线程争用非常严重; 
而LongAdder则有两个值用于累加,一个是base,它的作用类似于AtomicInteger里面的value,在没有竞争的情况不会用到cells数组,它为null,这时使用base做累加,有了竞争后cells数组就上场了,第一次初始化长度为2,以后每次扩容都是变为原来的两倍,直到cells数组的长度大于等于当前服务器cpu的数量为止就不在扩容(想下为什么到超过cpu数量的时候就不再扩容);每个线程会通过线程对cells[threadLocalRandomProbe%cells.length]位置的Cell对象中的value做累加,这样相当于将线程绑定到了cells中的某个cell对象上;
@sun.misc.Contended static final class Cell {
        //保存要累加的值
        volatile long value;
        Cell(long x) { value = x; }
        //使用Unsafe类的cas来更新value的值
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }
        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        // 获取value值在Cell对象中的偏移量,以便迅速定位
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

伪共享:见《jdk8的注解 @sun.misc.Contended

 

casBase()方法:

可以认为变量base就是第一个value值,也是基础value变量。先调用casBase函数来cas一下base变量,如果成功了,就不需要在进行后面比较复杂的算法。

    /**
     * CASes the base field.
     */
    final boolean casBase(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
    }

 

longAccumulate()方法:

longAccumulate比较复杂,
首先,我们都知道只有当对base的cas操作失败之后,LongAdder才引入Cell数组.所以在longAccumulate中就是对Cell数组进行操作.分别涉及了数组的初始化,扩容和设置某个位置的Cell对象等操作.
在这段代码中,关于cellBusy的cas操作构成了一个SpinLock,这就是经典的SpinLock的编程技巧,大家可以学习一下.
final void longAccumulate(long x, LongBinaryOperator fn,
                             boolean wasUncontended) {
       int h;
       if ((h = getProbe()) == 0) { //获取PROBE变量,探针变量,与当前运行的线程相关,不同线程不同
           ThreadLocalRandom.current(); //初始化PROBE变量,和getProbe都使用Unsafe类提供的原子性操作。
           h = getProbe();
           wasUncontended = true;
       }
       boolean collide = false;
       for (;;) { //cas经典无限循环,不断尝试
           Cell[] as; Cell a; int n; long v;
           if ((as = cells) != null && (n = as.length) > 0) { //分支1:cells数组不为null,并且数组size大于0
           //表示cells已经初始化了
               if ((a = as[(n - 1) & h]) == null) { //通过与操作计算出来需要操作的Cell对象的坐标
                   if (cellsBusy == 0) { //volatile 变量,用来实现spinLock,来在初始化和resize cells数组时使用。
                   //当cellsBusy为0时,表示当前可以对cells数组进行操作。 
                       Cell r = new Cell(x);//将x值直接赋值给Cell对象
                       if (cellsBusy == 0 && casCellsBusy()) {//如果这个时候cellsBusy还是0
                       //就cas将其设置为非0,如果成功了就是获得了spinLock的锁.可以对cells数组进行操作.
                       //如果失败了,就会再次执行一次循环
                           boolean created = false;
                           try {
                               Cell[] rs; int m, j;
                               //判断cells是否已经初始化,并且要操作的位置上没有cell对象.
                               if ((rs = cells) != null &&
                                   (m = rs.length) > 0 &&
                                   rs[j = (m - 1) & h] == null) {
                                   rs[j] = r; //将之前创建的值为x的cell对象赋值到cells数组的响应位置.
                                   created = true;
                               }
                           } finally {
                               //经典的spinLock编程技巧,先获得锁,然后try finally将锁释放掉
                               //将cellBusy设置为0就是释放锁.
                               cellsBusy = 0;
                           }
                           if (created)
                               break; //如果创建成功了,就是使用x创建了新的cell对象,也就是新创建了一个分担热点的value
                           continue; 
                       }
                   }
                   collide = false; //未发生碰撞
               }
               else if (!wasUncontended)//是否已经发生过一次cas操作失败
                   wasUncontended = true; //设置成true,以便第二次进入下一个else if 判断
               else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                            fn.applyAsLong(v, x))))
                   //fn是操作类型,如果是空,就是相加,所以让a这个cell对象中的value值和x相加,然后在cas设置,如果成果
                  //就直接返回
                   break;
               else if (n >= NCPU || cells != as)
                 //如果cells数组的大小大于系统的可获得处理器数量或在as不再和cells相等.
                   collide = false;            // At max size or stale
               else if (!collide)
                   collide = true;
               else if (cellsBusy == 0 && casCellsBusy()) {
                 //再次获得cellsBusy这个spinLock,对数组进行resize
                   try {
                       if (cells == as) {//要再次检测as是否等于cells以免其他线程已经对cells进行了操作.
                           Cell[] rs = new Cell[n << 1]; //扩容一倍
                           for (int i = 0; i < n; ++i)
                               rs[i] = as[i];
                           cells = rs;//赋予cells一个新的数组对象
                       }
                   } finally {
                       cellsBusy = 0;
                   }
                   collide = false;
                   continue;
               }
               h = advanceProbe(h);//由于使用当前探针变量无法操作成功,所以重新设置一个,再次尝试
           }
           else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
           //cells数组未初始化,获得cellsBusy lock,来初始化
               boolean init = false;
               try {                           // Initialize table
                   if (cells == as) {
                       Cell[] rs = new Cell[2];
                       rs[h & 1] = new Cell(x); //设置x的值为cell对象的value值
                       cells = rs;
                       init = true;
                   }
               } finally {
                   cellsBusy = 0;
               }
               if (init)
                   break;
           }//如果初始化数组失败了,那就再次尝试一下直接cas base变量,如果成功了就直接返回
           else if (casBase(v = base, ((fn == null) ? v + x :
                                       fn.applyAsLong(v, x))))
               break;                          // Fall back on using base
       }
   }

上面的代码主要有三个分支: 

分支一、 如果数组不为空 
分支二、 数据为空,则初始化 
分支三、 前面都更新失败了,尝试更新base数据 
4.cellBusy是一个标示元素,只有当修改cell数组大小或者插入元素的时候才会修改。

    /**
     * CASes the cellsBusy field from 0 to 1 to acquire lock.
     */
    final boolean casCellsBusy() {
        return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
    }

分支二、分支三都很简单,我们来重点分析一下分支一。 
当要更新的位置没有元素的时候,首先cas标志位,防止扩容以及插入元素,然后插入数据。如果成功直接返回,否则标示发生了冲突,然后重试。如果对应的位置有元素则更新,如果更新失败,进行判断是否数组的大小已经超过了cpu的核数,如果大于的话,则意味着扩容没有意义。直接重试。否则进行扩容,扩容完成后,重新设置要更新的位置,尽可能保证下一次更新成功。 

 

 

 

 

成员变量cells

cells数组是LongAdder高性能实现的必杀器: 
AtomicInteger只有一个value,所有线程累加都要通过cas竞争value这一个变量,高并发下线程争用非常严重; 
而LongAdder则有两个值用于累加,一个是base,它的作用类似于AtomicInteger里面的value,在没有竞争的情况不会用到cells数组,它为null,这时使用base做累加,有了竞争后cells数组就上场了,第一次初始化长度为2,以后每次扩容都是变为原来的两倍,直到cells数组的长度大于等于当前服务器cpu的数量为止就不在扩容(想下为什么到超过cpu数量的时候就不再扩容);每个线程会通过线程对cells[threadLocalRandomProbe%cells.length]位置的Cell对象中的value做累加,这样相当于将线程绑定到了cells中的某个cell对象上;
 

成员变量cellsBusy

cellsBusy,它有两个值0 或1,它的作用是当要修改cells数组时加锁,防止多线程同时修改cells数组,0为无锁,1为加锁,加锁的状况有三种 
1. cells数组初始化的时候; 
2. cells数组扩容的时候; 
3. 如果cells数组中某个元素为null,给这个位置创建新的Cell对象的时候;

成员变量base

它有两个作用: 
1. 在开始没有竞争的情况下,将累加值累加到base 
2. 在cells初始化的过程中,cells不可用,这时会尝试将值累加到base上;
posted on 2013-12-13 09:40  duanxz  阅读(1400)  评论(0编辑  收藏  举报