并发系列——JUC高级篇(二)原子操作类

JUC 包提供了一系列的原子性操作类,这些类都是使用非阻塞算法 CAS 实现的,相 比使用锁实现原子性操作这在性能上有很大提高。由于原子性操作类的原理都大致相同, 所以本章只讲解最简单的 AtomicLong 类的实现原理以及 JDK 8 中新增 的 LongAdder 和 LongAccumulator 类的原理。有了这些基础, 再去理解其他原子性操作类的实现就不会感到困难了 。

原子包:java.util.concurrent.atomic包下的操作类

标量类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

复合变量类:AtomicMarkableReference,AtomicStampedReference

原子操作包:

    Atomic包是java.util.concurrent下的另一个专门为线程安全设计的Java包,包含多个原子操作类。这个包里面提供了一组原子变量类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。

   多线程下将属性设置为atomic可以保证读取数据的一致性。因为他将保证数据只能被一个线程占用,也就是说一个线程对属性进行写操作时,会使用自旋锁锁住该属性。不允许其他的线程对其进行读取操作了。
但是它有一个很大的缺点:因为它要使用自旋锁锁住该属性,因此它会消耗更多的资源,性能会很低。要比nonatomic慢20倍。

一、 原子变量操作类( Atomiclnteger、 AtomicLong 和 AtomicBoolean )

JUC 并发包中包含有 Atomiclnteger、 AtomicLong 和 AtomicBoolean 等原子性操作类, 它们的原理类似,本章讲解 AtomicLong 类。 AtomicLong 是原子性递增或者递减类,其内 部使用 Unsafe 来实现,我们看下面的代码。

1、构造方法及基本属性等:

/**
一个long值可以用原子更新。 有关原子变量属性的描述,请参阅java.util.concurrent.atomic包规范。 
一个AtomicLong用于诸如原子增量的序列号的应用中,不能用作Long的替代物 。 但是,该类确实扩展了Number ,以允许使用基于数字类的工具和实用程序的统一访问。 

 */
public class AtomicLong extends Number implements java.io.Serializable {

/*serialVersionUID 有什么作用?该如何使用?
serialVersionUID 是实现 Serializable 接口而来的,而 Serializable 则是应用于Java 对象序列化/反序列化。对象的序列化主要有两种用途:
1. 把对象序列化成字节码,保存到指定介质上(如磁盘等)
2. 用于网络传输
详情看下面参考文献1  
这是链接https://github.com/giantray/stackoverflow-java-top-qa/blob/master/contents/what-is-a-serialversionuid-and-why-should-i-use-it.md
 */  
  
    private static final long serialVersionUID = 1927816293512124184L;

    // 设置为使用Unsafe.compareAndSwapInt进行更新,获取usafe实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
   //偏移量
    private static final long valueOffset;

/*
记录底层JVM是否支持longs的无锁compareAndSwap。 
虽然Unsafe.compareAndSwapLong方法在任何一种情况下都可以工作,
但是应该在Java级别处理一些构造以避免锁定用户可见的锁。
 */    
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();

/*
返回底层JVM是否支持longs的无锁CompareAndSet。 仅调用一次并缓存在VM_SUPPORTS_LONG_CAS中。
 */
    private static native boolean VMSupportsCS8();

/*  
下面这段静态代码块是用于获取valueOffset(偏移量)
 */
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

//  这个就是AtomicBoolean的关键值,value
    private volatile long value;

    /**
     * 用给定的初始值创建一个新的AtomicLong
     * @param initialValue initialValue - 初始值 
     */
    public AtomicLong(long initialValue) {
        value = initialValue;
    }

    /**
     * 创建一个新的AtomicLong,初始值为 0 。 
     */
    public AtomicLong() {
    }


//其他的一些方法

}

代码通过 Unsafe.getUnsafe ( )方法获取到 Unsafe 类的实例, 这里你可能会有疑问, 为何能通过 Unsafe.getUnsafe ( )方法获取到 Unsafe 类的实例?其实这是因为 AtomicLong 类也是在 rt.jar 包下面的, AtomicLong 类就是通过 BootStarp 类加载器进行加载的。(只有通过 BootStarp 类加载器进行加载的才能通过 Unsafe.getUnsafe ( )方法获取到 Unsafe 类的实例)。代码 中的 value 被声 明为 volatile 的,这是为了在多线程下保证内存可见性, value 是具体存放计数的变量。

2、其他的方法:

递增和递减操作代码(1.8)

public final long getAndIncrement() {//调用unsafe方法, 原子性设置value值为原始值+1 ,返回值为递增后的值 (即获取某个位置的值然后该值执行++操作)
    return unsafe.getAndAddLong(this, valueOffset, 1L);
}

public final long getAndDecrement() {//调用 unsafe方法,原子性设置value{直为原始值-1 ,返回值为递减之后的值 
    return unsafe.getAndAddLong(this, valueOffset, -1L);
}

public final long incrementAndGet() {  //调用unsafe方法,原子性设置value值为原始值吐, 返回值为原始值
    return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}

public final long decrementAndGet() {  //调用 unsafe方法, !fr,、子性设置value{直为原始值-1 ,返回位为原始值 
    return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
}

上述代码中,valueOffset为AtomicLong在static语句块中进行初始化时通过Unsafe类获得的本类中value属性的内存偏移值。

可以看到,上述四个方法都是基于Unsafe类中的getAndAddLong方法实现的。

getAndAddLong源码如下

public final long getAndAddLong(Object var1, long var2, long var4) {
    long var6;
    //CAS操作设置var1对象偏移为var2处的值增加var4
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

    return var6;
}

compareAndSet方法

public final boolean compareAndSet(long expect, long update) {
    return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}

可见,内部还是调用了Unsafe类中的CAS方法。

AtomicLong使用示例

public class AtomicLongDemo {
    private static AtomicLong al = new AtomicLong(0);

    public static long addNext() {
        return al.getAndIncrement();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread() {
                @Override
                public void run() {
                    AtomicLongDemo.addNext();
                }
            }.start();
        }

        // 等待线程运行完
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("final result is " + AtomicLongDemo.addNext());
    }
}

AtomicLong使用CAS非阻塞算法,性能比使用synchronized等的阻塞算法实现同步好很多。但在高并发下,大量线程会同时去竞争更新同一个原子变量,由于同时只有一个线程的CAS会成功,会造成大量的自旋尝试,十分浪费CPU资源。因此,JDK8中新增了原子操作类LongAdder。(自旋锁循环次数多少?)

二、JDK 8 新增的原子操作类 LongAdder

前面讲过, AtomicLong 通过 CAS 提供了非阻塞的原子性操作,相 比使用阻塞算法的 同步器来说它的性能己经很好了,但是 JDK 开发组并不满足于此。 使用 AtomicLong 时, 在高并发下大量线程会同时去竞争更新同→个原子变量,但是由于同时只有一个线程的 CAS 操作会成功,这就造成了大量线程竞争失败后,会通过无限循环不断进行自旋尝试 CAS 的操作, 而这会白白浪费 CPU 资源。
因此 JDK 8 新增了一个原子性递增或者递减类 LongAdder 用来克服在高并发下使用 AtomicLong 的缺点。 既然 AtomicLong 的性能瓶颈是由于过多线程同时去竞争一个变量的 更新而产生的,那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源, 是不是就解决了性能问题?是的, LongAdder 就是这个思路。

由上可知,AtomicLong的性能瓶颈是多个线程同时去竞争一个变量的更新权导致的。而LongAdder通过将一个变量分解成多个变量,让同样多的线程去竞争多个资源解决了此问题。

原理

如图,LongAdder内部维护了多个Cell,每个Cell内部有一个初始值为0的long类型变量,这样,在同等并发下,对单个变量的争夺会变少。此外,多个线程争夺同一个变量失败时,会到另一个Cell上去尝试,增加了重试成功的可能性。当LongAdder要获取当前值时,将所有Cell的值于base相加返回即可。

LongAdder维护了一个初始值为null的Cell数组和一个基值变量base。当一开始Cell数组为空且并发线程较少时,仅使用base进行累加。当并发增大时,会动态地增加Cell数组的容量。

Cell类中使用了@sun.misc.Contented注解进行了字节填充(解决伪共享问题),解决了由于连续分布于数组中且被多个线程操作可能造成的伪共享问题(关于伪共享,可查看《伪共享(false sharing),并发编程无声的性能杀手》这篇文章)。

注意:这里的cell和currenthashmap1.8计算map的size时的算法是一样的:可以看我之前对currenthashmap1.8的size个数计算解析看看1.8的ConcurrentHashMap源码

(这里对某个value进行修改相当于concurrenthashmap中的size个数,即用cell数组来存储其他竞争失败的线程对该value的操作记录,在要获取该value时再去加载cell数组里的操作以达到最终的值。)

注意:这里是多个线程对value进行更新时,由于有多个线程进行自旋消耗大量性能(多个线程更新value时没竞争成功操作CAS的线程是要进行自旋等待的),此时LondAdddr采用先让线程存对value更新的操作(用cell数组记录),然后等需要value值时+cell数组的更新得到最后的值(这个过程也是CAS操作,相对于多个线程自旋效率更高)(用到时再去加载:懒加载思想),LongAdder即优化了多个线程更新同一个原子变量的操作。

 

下面看看LongAdder的源码:

先看LongAdder的定义

public class LongAdder extends Striped64 implements Serializable

Striped64类中有如下三个变量:

transient volatile Cell[] cells;

transient volatile long base;

transient volatile int cellsBusy;

cellsBusy用于实现自旋锁,状态值只有0和1,当创建Cell元素、扩容Cell数组或初始化Cell数组时,使用CAS操作该变量来保证同时只有一个变量可以进行其中之一的操作。

下面看Cell的定义:

@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);
    }

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

将value声明伪volatile确保了内存可见性,CAS操作保证了value值的原子性,@sun.misc.Contented注解的使用解决了伪共享问题。

下面来看LongAdder中的几个方法:

  • long Sum()
public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

sum的结果并非一个精确值,因为计算总和时并没有对Cell数组加锁,累加过程中Cell的值可能被更改。

  • void reset()
public void reset() {
    Cell[] as = cells; Cell a;
    base = 0L;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                a.value = 0L;
        }
    }
}

reset非常简单,将base和Cell数组中非空元素的值置为0.

  • long sumThenRest()
public long sumThenReset() {
    Cell[] as = cells; Cell a;
    long sum = base;
    base = 0L;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null) {
                sum += a.value;
                a.value = 0L;
            }
        }
    }
    return sum;
}

sumThenReset同样非常简单,将某个Cell的值加到sum中后随即重置。

  • void add(long x)
public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    // 判断cells是否为空,如果不为空则直接进入内层判断,
    // 否则尝试通过CAS在base上进行add操作,若CAS成功则结束(不成功则要进去内层CAS),否则进入内层
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        // 记录cell上的CAS操作是否失败
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            // 计算当前线程应该访问cells数组的哪个元素(threadLocalRandomProbe&(legth-1)求得访问的数组下标)
            // getProbe()则用于获取当前线程中变量 threadLocalRandomProbe 的 值,这个值一开始为 0(可以理解为线程本身的hash值)
            (a = as[getProbe() & m]) == null ||
            // 尝试通过CAS操作在对应cell上add(再次尝试CAS加上cell的value值)
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

add方法会判断cells数组是否为空,非空则进入内层,否则直接通过CAS操作在base上进行add。内层代码中,声明了一个uncontented变量来记录调用longAccumulate方法前在相应cell上是否进行了失败的CAS操作。

下面重点来看longAccumelate方法:

longAccumulate时Striped64类中定义的,其中包含了初始化cells数组,改变cells数组长度,新建cell等逻辑。

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
    int h;
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current(); // 初始化当前线程的probe,以便于找到线程对应的cell
        h = getProbe();
        wasUncontended = true; // 标记执行longAccumulate前对相应cell的CAS操作是否失败,失败为false
    }
    boolean collide = false; // 是否冲突,如果当前线程尝试访问的cell元素与其他线程冲突,则为true
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        //A: 当前cells不为空且元素个数大于0则进入内层,否则尝试初始化(即B)
        if ((as = cells) != null && (n = as.length) > 0) {
            //A1:该数组元素为null
            if ((a = as[(n - 1) & h]) == null) { 
                if (cellsBusy == 0) {       // 尝试添加新的cell(用于实现自旋锁,状态值只有0和1,0则没有其他线程干预)
                    Cell r = new Cell(x);    //初始化将x传进去
                    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;
                    }
                }
                collide = false;
            }
            // A2:如果已经进行了失败的CAS操作
            else if (!wasUncontended)  
                wasUncontended = true; // 则不调用下面的a.cas()函数(反正肯定是失败的),而是重新计算probe值来尝试
            //A3:尝试通过CAS操作在value上进行add
            else if (a.cas(v = a.value, ((fn == null) ? v + x :fn.applyAsLong(v, x))))
                break;
             // A4:如果当前cells长度大于CPU个数则不进行扩容,因为每个cell都使用一个CPU处理时性能才是最高的
// 如果当前cells已经过时(其他线程对cells执行了扩容操作,改变了cells指向),也不会扩容
            else if (n >= NCPU || cells != as)
                collide = false; 
             //A5:不冲突
            else if (!collide)
                collide = true;  // 执行到此处说明a.cas()执行失败,即有冲突,将collide置为true,
                                 // 跳过扩容阶段,重新获取probe,到cells不同位置尝试cas,再次失败则扩容
            // A6:扩容,如果当前元素个数没有达到CPU个数并且有冲突则扩容(A4、A5不成立的情况扩容)
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                    if (cells == as) {      
                        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; // 扩容后再次尝试(扩容后cells长度改变,
                          // 根据(n - 1) & h计算当前线程在cells中对应元素下标会变化,减少再次冲突的可能性)
            }
            h = advanceProbe(h); // 重新计算线程probe,减小下次访问cells元素时的冲突机会
        }
        //B: 初始化cells数组
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           
                if (cells == as) {
                    Cell[] rs = new Cell[2];
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        // C:初始化失败,尝试通过base的CAS操作进行add,成功则结束当前函数,否则再次循环
        else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))
            break; 
    }
}

代码比较复杂,细节的解释都写在注释中了。大体逻辑就是判断cells是否为空或者长度为0:如果空或者长度为0则尝试进行cells数组初始化(B),初始化失败的话则尝试通过CAS操作在base上进行add,仍然失败则重走一次流程(C);如果cells不为空且长度大于0,则获取当前线程对应于cells中的元素,如果该元素为null则尝试创建(A1),否则尝试通过CAS操作在上面进行add(A3),仍失败则扩容(A6,value值冲突且当前cells元素个数小于cpu数则进行扩容)(A)。(扩完容重新计算线程probe,减小下次访问cells元素时的冲突机会)

总结: ( 1 ) LongAdder 的结构是怎样的?上面所述

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

threadLocalRandomProbe&(legth-1)求得访问的数组下标。这个值一开始为 0(可以理解为线程本身的hash值)

( 3) 如何初始化 Cell 数组?上面所述

(4) Cell 数组如何扩容? 扩容的条件是什么?

扩容为原先的2倍(默认初始大小是2)。如果当前元素个数小于CPU个数(1、尽量一个线程对应一个cpu)并且有冲突(2、同一个元素value有其他线程冲突要更新)则扩容。

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

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

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

并且当前线程通过分配的 Cell 元素的 cas 函数来保证对 Cell 元素 value 值更新的原子性。

三、LongAccumulator 类

LongAdder 类是 LongAccumulator 的一个特例, LongAccumulator 比 LongAdder 的功能 更强大。 例如下面的构造函数, 其中 accumulatorFunction 是一个双目运算器接口, 其根据 输入的两个参数返回一个计算值, identity 则是 LongAccumulator 累加器的初始值。(简单的说LongBinaryOperator  accumulatorFunction参数可以是非null的,可以是计算函数等或自定义函数)

LongAdder是LongAccumulator的特例,两者都继承自Striped64。

看如下代码:

public LongAccumulator(LongBinaryOperator accumulatorFunction,
                        long identity) {
    this.function = accumulatorFunction; //LongAdder的accumulatorFunction是null(即其中的一种情况)
    base = this.identity = identity;
}

public interface LongBinaryOperator {
    long applyAsLong(long left, long right);
}

LongAccumulator构造器允许传入一个双目运算符接口用于自定义加法规则,还允许传入一个初始值。

自定义的加法函数是如何被应用的呢?以上提到的longAccumulate()方法中有如下代码:

a.cas(v = a.value, ((fn == null) ? v + x :fn.applyAsLong(v, x)))  //

LongAdder的add()方法中调用longAccumulate()方法时传入的是null,而LongAccumulator的accumulate()方法传入的是this.function(传入加法函数/自定义的计算函数),即自定义的加法函数。

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

总结 : 本节简单介绍 了 LongAccumulator 的原理。 LongAdder 类是 LongAccumulator 的一个特例,只是后者提供了更加强大的功能, 可以让用户自定义累加规则。

总结本篇:先解析AtomicLong用CAS+volatile实现非阻塞方式比较加锁等阻塞锁有了大的性能提升,然后发现AtomicLong存在多线程更新同一个原子值会出现多个线程自旋等待的性能消耗,此时提出了LongAdder用cells数组来记录对原子值的更新值,等需要求最后的原子值时再去累加cells数组的value值,这样可以少了自旋带来的消耗。最后介绍了LongAccumulator类。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值