第四章、JUC包中原子操作类原理剖析

196 阅读2分钟

JUC包提供了一系列的原子性操作类,这些类都是使用非阻塞算法CAS实现的,相比使用锁实现原子性操作这在性能上有很大提高。

接下来介绍AtomicLong类的实现原理以及JDK 8中新增的LongAdder和LongAccumulator类的原理。

1.递增和递减操作代码

// 调用Unsafe方法,原子性的将value值+1之后 返回更新值 
public final long incrementAndGet() { 
    return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L; 
} 
// 调用Unsafe方法,原子性的将value值+1之后 返回更新值 
public final long decrementAndGet() { 
    return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L; 
} 
// 调用Unsafe方法,原子性的将value值+1之后 返回原始值 
public final long getAndIncrement() { 
    return unsafe.getAndAddLong(this, valueOffset, 1L); 
} 
// 调用Unsafe方法,原子性的将value值+1之后 返回原始值 
public final long getAndDecrement() { 
    return unsafe.getAndAddLong(this, valueOffset, -1L); 
}

2.举例 统计0的个数

/**
 * 统计 0 的个数
 */
public class AtomicDemo {
    public static AtomicLong atomicLong = new AtomicLong();
    private static Integer[] a1 = {0, 1, 2, 3, 21, 0, 2, 5, 0};
    private static Integer[] a2 = {2, 4, 56, 0, 32, 0, 1};

    public static void main(String[] args) throws InterruptedException {
        Thread thread0 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0; i<a1.length; i++){
                    if (a1[i] == 0){
                        atomicLong.incrementAndGet();
                    }
                }
            }
        });

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0; i<a2.length; i++){
                    if (a2[i] == 0){
                        atomicLong.incrementAndGet();
                    }
                }
            }
        });

        thread0.start();
        thread1.start();

        thread0.join();
        thread1.join();
        System.out.println("count 0:" + atomicLong);
    }
}

在没有原子类的情况下,实现计数器需要使用一定的同步措施,比如使用synchronized关键字等,但是这些都是阻塞算法,对性能有一定损耗,而本章介绍的这些原子操作类都使用CAS非阻塞算法,性能更好。

3.LongAdder

使用AtomicLong时,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS操作会成功,这就造成了大量线程竞争失败后,会通过无限循环不断进行自旋尝试CAS的操作,而这会白白浪费CPU资源。 JDK 8新增了一个原子性递增或者递减类LongAdder用来克服在高并发下使用AtomicLong的缺点。

实现原理:把一个变量分解为多个变量,让同样多的线程去竞争多个资源。

clipboard.png

如图所示,LongAdder维护了一个延迟初始化的原子性更新数组(默认情况下Cell数组是null)和一个基值变量base。在同等并发量的情况下,减少了争夺共享资源的并发量。另外,多个线程在争夺同一个Cell原子变量时如果失败了,它并不是在当前Cell变量上一直自旋CAS重试,而是尝试在其他Cell的变量上进行CAS尝试,这个改变增加了当前线程重试CAS成功的可能性。最后,在获取LongAdder当前值时,是把所有Cell变量的value值累加后再加上base返回的。

下面围绕以下话题从源码角度来分析LongAdder的实现:

(1)LongAdder的结构是怎样的?

1.png

LongAdder的真实值其实是base的值与Cell数组里面所有Cell元素中的value值的累加,base是个基础值,默认为0。cellsBusy用来实现自旋锁,状态值只有0和1。

(2)当前线程应该访问Cell数组里面的哪一个Cell元素?

当前线程应该访问cells数组的哪一个Cell元素是通过getProbe()& m进行计算的,其中m是当前cells数组元素个数-1, getProbe()则用于获取当前线程中变量threadLocalRandomProbe的值,这个值一开始为0,在以下代码中初始化。 longAccumulate(x, null, uncontended);

(3)如何初始化Cell数组?

其中cellsBusy是一个标示,为0说明当前cells数组没有在被初始化或者扩容,也没有在新建Cell元素,为1则说明cells数组在被初始化或者扩容,或者当前在创建新的Cell元素、通过CAS操作来进行0或1状态的切换,这里使用casCellsBusy函数。假设当前线程通过CAS设置cellsBusy为1,则当前线程开始初始化操作,那么这时候其他线程就不能进行扩容了。

初始化cells数组元素个数为2,然后使用h&1计算当前线程应该访问celll数组的哪个位置,也就是使用当前线程的threadLocalRandomProbe变量值&(cells数组元素个数-1),然后标示cells数组已经被初始化,最后又重置了cellsBusy标记。

// 初始化 cells数组
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;
}

(4)Cell数组如何扩容? 具体就是当前cells的元素个数小于当前机器CPU个数并且当前多个线程访问了cells中同一个元素,从而导致冲突使其中一个线程CAS失败时才会进行扩容操作。

// 当前cells数组元素个数大于CPU个数
else if (n >= NCPU || cells != as)
    collide = false;            // At max size or stale
    // 是否有冲突
else if (!collide)
    collide = true;
    // 如果cells数组元素个数没有达到CPU个数并且有冲突
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
}

(5)线程访问分配的Cell元素有冲突后如何处理?

对CAS失败的线程重新计算当前线程的随机值threadLocalRandomProbe,以减少下次访问cells元素时的冲突机会

/* 
 * 为了能够找到一个空闲的Cell,重新计算hash值,xorshift算法生成随机数 
*/
h = advanceProbe(h);

(6)如何保证线程操作被分配的Cell元素的原子性?

Cell的构造很简单,其内部维护一个被声明为volatile的变量,这里声明为volatile是因为线程操作value变量时没有使用锁,为了保证变量的内存可见性。另外cas函数通过CAS操作,保证了当前线程更新时被分配的Cell元素中value值的原子性。另外,Cell类使用@sun.misc.Contended修饰是为了避免伪共享。

@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }
}

4.LongAccumulator

LongAdder类是LongAccumulator的一个特例

LongAccumulator相比于LongAdder,可以为累加器提供非0的初始值,后者只能提供默认的0值。另外,前者还可以指定累加规则,比如不进行累加而进行相乘,只需要在构造LongAccumulator时传入自定义的双目运算器即可,后者则内置累加的规则。

LongAccumulator相比于LongAdder的不同在于,在调用casBase时后者传递的是b+x,前者则使用了r = function.applyAsLong(b = base, x)来计算。