• java原子类详解


    java原子类详解


    什么原子类


    原子类是具有原子性的类,原子性的意思是对于一组操作,要么全部执行成功,要么全部执行失败,不能只有其中某几个执行成功。

    原子类作用


    作用和锁有类似之处,是为了保证并发情况下的线程安全。

    相对于锁的优势


    粒度更细
    原子变量可以把竞争范围缩小到变量级别,通常情况下锁的粒度也大于原子变量的粒度

    效率更高
    除了在高并发之外,使用原子类的效率往往比使用同步互斥锁的效率更高,因为原子类底层利用了CAS,不会阻塞线程。

    在JDK中J.U.C包下提供了种类丰富的原子类,以下所示:

    类型    具体类型
    Atomic* 基本类型原子类    AtomicInteger、AtomicLong、AtomicBoolean
    Atomic*Array 数组类型原子类    AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
    Atomic*Reference 引用类型原子类    AtomicReference
    Atomic*FieldUpdater 升级类型原子类    AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
    Adder 累加器    LongAdder、DoubleAdder
    Accumulator 积累器    LongAccumulator、DoubleAccumulator

    AtomicInteger常用方法


    上面列举了J.U.C中提供的一些原子操作类,接下来从简单的AtomicInteger开始分析,来看看它的常用方法,其他的两种AtomicLong、AtomicBoolean和它相似

    方法    作用
    public final int get()    获取当前的值
    public final int getAndSet(int newValue)    获取当前的值,并设置新的值
    public final int getAndIncrement()    获取当前的值,并自增+1
    public final int getAndDecrement()    获取当前的值,并自减-1
    public final int getAndAdd(int delta)    获取当前的值,并加上预期的值。
    boolean compareAndSet(int expect, int update)    如果输入的数值等于预期值,则以原子方式将该值更新为输入值(update)


    Array 数组类型原子类


    AtomicArray 数组类型原子类,数组里的元素,都可以保证其原子性,比如 AtomicIntegerArray 相当于把 AtomicInteger 聚合起来,组合成一个数组。我们如果想用一个每一个元素都具备原子性的数组的话, 就可以使用 AtomicArray

    该类包括:

    类名    作用
    AtomicIntegerArray    整形数组原子类
    AtomicLongArray    长整形数组原子类
    AtomicReferenceArray    引用类型数组原子类
    Atomic*Reference 引用类型原子类
    AtomicReference引用类型原子类,作用和AtomicInteger没有本质区别,AtomicReference是让一个对象保持原子性,而不局限一个变量。

    reference该种类所有类型:

    类名    作用
    AtomicReference    保证对象的原子性
    AtomicStampedReference    它是对 AtomicReference 的升级,在此基础上还加了时间戳,用于解决 CAS 的 ABA 问题。
    AtomicMarkableReference    和 AtomicReference 类似,多了一个绑定的布尔值,可以用于表示该对象已删除等场景。


    Atomic*FieldUpdater原子更新器


    原子类更新器主要用于对已经声明的非原子变量,为它增加原子性,让该变量拥有CAS操作的能力。
    该种类所有类型:

    类名    作用
    AtomicIntegerFieldUpdater    原子更新整形的更新器
    AtomicLongFieldUpdater    原子更新长整形的更新器
    AtomicReferenceFieldUpdater    原子更新引用的更新器

    1. public class AtomicIntegerFieldUpdaterDemo implements Runnable {
    2. static Score useUpdaterScore;
    3. static Score unusedComputerScore;
    4. //声明原子更新类,泛型为Score类,更新的是"score"字段
    5. public static AtomicIntegerFieldUpdater<Score> scoreUpdater = AtomicIntegerFieldUpdater
    6. .newUpdater(Score.class, "score");
    7. @Override
    8. public void run() {
    9. for (int i = 0; i < 1000; i++) {
    10. //非原子操作直接自增
    11. unusedComputerScore.score++;
    12. //使用更新器更新字段,同样执行自增
    13. scoreUpdater.getAndIncrement(useUpdaterScore);
    14. }
    15. }
    16. public static class Score {
    17. volatile int score;
    18. }
    19. public static void main(String[] args) throws InterruptedException {
    20. useUpdaterScore = new Score();
    21. unusedComputerScore = new Score();
    22. AtomicIntegerFieldUpdaterDemo r = new AtomicIntegerFieldUpdaterDemo();
    23. Thread t1 = new Thread(r);
    24. Thread t2 = new Thread(r);
    25. t1.start();
    26. t2.start();
    27. t1.join();
    28. t2.join();
    29. System.out.println("普通变量的结果:" + unusedComputerScore.score);
    30. System.out.println("升级后的结果:" + useUpdaterScore.score);
    31. }
    32. }

    输出结果:
    普通变量的结果:1980
    升级后的结果:2000

    从结果可以看出,升级后结果是期望值。

    使用场景(相对于直接使用AtomicInteger)
    历史原因
    如果当前变量在之前开发版本中使用地方过多,这个时候为了不做大量改造,可以在需要原子性的时候,使用原子更新器
    少部分情况需要原子性的时候
    由于原子类型的变量比普通变量更耗资源。在大部分不需要原子性的时候,如果都设置成原子类型,这非常的耗资源。因此,我们可以再需要原子性的少数情况下,对当前变量使用AtomicIntegerFieldUpdater进行合理的升级。


    原子类是如何利用 CAS 保证线程安全的?
    1. public final int getAndAdd(int delta) {
    2. return unsafe.getAndAddInt(this, valueOffset, delta);
    3. }

    是Unsafe类

    该方法实际调用的是,unsafe.getAndAddInt(this, VALUE, delta),这里先介绍一下Unsafe类。

    Unsafe类
    Unsafe是CAS的核心类。由于java无法直接访问底层操作系统,而是需要通过本地方法来实现。JVM还是提供了Unsafe类,他提供了硬件层面的原子操作,可以直接操作内存的数据。
     

    1. public class AtomicInteger extends Number implements java.io.Serializable {
    2. private static final long serialVersionUID = 6214790243416807050L;
    3. private static final Unsafe unsafe = Unsafe.getUnsafe();
    4. private static final long valueOffset;
    5. private volatile int value;
    6. static {
    7. try {
    8. valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    9. } catch (Exception var1) {
    10. throw new Error(var1);
    11. }
    12. }
    13. private volatile int value;
    14. public final int get() {
    15. return value;
    16. }
    17. ......
    18. }
    1. public final int getAndAddInt(Object var1, long var2, int var4) {
    2. int var5;
    3. do {
    4. var5 = this.getIntVolatile(var1, var2);
    5. } while(!this.compareAndSwapInt(var1, var2, var5, var5 +var4));
    6. return var5;
    7. }

     

    首先获取Unsafe类型变量unsafe,并定义变量valueOffset,然后在静态代码块中value值在内存中的偏移地址,赋值给valueOffset变量。因为 Unsafe 就是根据内存偏移地址获取数据的原值的,这样我们就能通过 Unsafe 来实现 CAS 了。

    value是用volatile修饰的,他就是我们原子类存储值的变量,由于使用volatile修饰,所以在多线程中看到的value值都是同一份,保证了可见性。

    接下来看实际执行cas的地方。Unsafe的getAndAddInt()方法

    先看do-while循环,它是一个无限循环,知道满足条件才退出循环。do中的代码时,获取到当前内存中的值赋值给var5,var1是当前原子对象,var2是value在内存中的偏移量。此时var5就是此时原子类的数值。

    再看看while中的退出条件。compareAndSwapInt这个方法参数,他们的实际意义是:

    第一个参数 --> 当前原子类对象,即AtomicInteger 这个对象本身
    第二个参数 --> 当前值的内存偏移量,接触它可以获取到value的值
    第三个参数 --> 当前值,如果当前值不匹配,更新失败会返回false,开始下次循环。如果当前值匹配,同时更新成功,方法返回true,跳出循环,完成当前累加操作
    第四个参数 --> 期望值,即当前值加上累加的值。
    所以 compareAndSwapInt 方法的作用就是,判断如果现在原子类里 value 的值和之前获取到的 var5 相等的话,那么就把计算出来的 var5 + var4 给更新上去,所以说这行代码就实现了 CAS 的过程。

    总结一下,Unsafe 的 getAndAddInt 方法是通过循环 + CAS 的方式来实现的,在此过程中,它会通过 compareAndSwapInt 方法来尝试更新 value 的值,如果更新失败就重新获取,然后再次尝试更新,直到更新成功。
     

    AtomicLong 高并发下存在的问题

    在并发情况下,如果我们需要实现计数器(例如下载任务数),可以利用AtomicInteger和AtomicLong,这样一来可以避免加锁和复杂的代码逻辑。虽然它们好用,但是也存在着一些问题。

    1. public class AtomicLongDemo {
    2. public static void main(String[] args) throws InterruptedException {
    3. AtomicLong counter = new AtomicLong(0);
    4. ExecutorService service = Executors.newFixedThreadPool(16);
    5. for (int i = 0; i < 100; i++) {
    6. service.submit(new Task(counter));
    7. }
    8. Thread.sleep(2000);
    9. System.out.println(counter.get());
    10. }
    11. static class Task implements Runnable {
    12. private final AtomicLong counter;
    13. public Task(AtomicLong counter) {
    14. this.counter = counter;
    15. }
    16. @Override
    17. public void run() {
    18. counter.incrementAndGet();
    19. }
    20. }
    21. }

    在这里,定义了一个为0的AtomicLong变量counter,然后创建了线程池数为16的线程池。然后执行100个任务。然后任务是执行counter.incrementAndGet()。结果毫无疑问是100,虽然并发执行,但是AtomicLong依然能保证incrementAndGet是一个原子操作,所以不会发生线程安全问题

    不过我们仔细看细节,对于 AtomicLong 内部的 value 属性而言,也就是保存当前 AtomicLong 数值的属性,它是被 volatile 修饰的,所以它需要保证自身可见性。
    当线程1执行incrementAndGet操作更新成功后,会将值向主内存中修改,同时主内存会将其他线程的value给修改掉,而且CAS也会经常失败,这两个操作是非常耗资源的。
     

    在JDK 8中新增了LongAdder 类,来优化这一个问题。

    我们将上面例子中做小小的调整,将AtomicLong换成LongAdder,get()方法换成sum(),incrementAndGet()换成increment()

    1. public class LongAdderDemo {
    2. public static void main(String[] args) throws InterruptedException {
    3. LongAdder counter = new LongAdder();
    4. ExecutorService service = Executors.newFixedThreadPool(16);
    5. for (int i = 0; i < 100; i++) {
    6. service.submit(new Task(counter));
    7. }
    8. Thread.sleep(2000);
    9. System.out.println(counter.sum());
    10. }
    11. static class Task implements Runnable {
    12. private final LongAdder counter;
    13. public Task(LongAdder counter) {
    14. this.counter = counter;
    15. }
    16. @Override
    17. public void run() {
    18. counter.increment();
    19. }
    20. }
    21. }

    行得到的结果还是100,但是运行速度比刚才AtomicLong实现要快。

    1. //集成自Striped64
    2. public class LongAdder extends Striped64 implements Serializable {}
    3. abstract class Striped64 extends Number {
    4. ......
    5. /**
    6. * 单元格表。如果为非null,则大小为2的幂。
    7. */
    8. transient volatile Cell[] cells;
    9. /**
    10. * 基本值,主要在没有争用时使用,也用作表初始化过程中的回退。通过CAS更新。
    11. */
    12. transient volatile long base;
    13. ......
    14. }

    LongAdder中引入了分段累加的概念,内部的Cell[]数组和base变量都参与了计数

    其中base在竞争不激烈的情况下,直接把累加的结果改到base变量上;
    在竞争激烈的时候,各个线程会分散累加到自己所对应的Cell[] 数组的某一个对象中,而不是公用一个。LongAdder这样分段的思想将不同线程到不同Cell上的修改,避免了大量的冲突,提升了并发性。更JDK 7 中的ConcurrentHashMap思想相似。

    竞争激烈的时候,LongAdder 会通过计算出每个线程的 hash 值来给线程分配到不同的 Cell 上去,每个 Cell 相当于是一个独立的计数器,这样一来就不会和其他的计数器干扰,Cell 之间并不存在竞争关系,所以在自加的过程中降低了冲突的概率。这个思想的本质就是空间换时间,所以会耗费更多的内存。

    最终的结果是通过LongAdder#sum()方法来获取的,将各个Cell值累计求和,再加上base返回。

    1. public long sum() {
    2. Cell[] as = cells;
    3. long sum = base;
    4. if (as != null) {
    5. for (Cell a : as)
    6. if (a != null)
    7. sum += a.value;
    8. }
    9. return sum;
    10. }

     

    AtomicLong和LongAdder使用如何选择


    在低竞争的情况下,AtomicLong和LongAdder的性能相似;但是在竞争激烈的情况下,LongAdder 的预期吞吐量要高得多,经过试验,LongAdder 的吞吐量大约是 AtomicLong 的十倍,虽然性能提高了,但是LongAdder会耗费更多的空间

    LongAdder 只提供了 add、increment 等简单的方法,适合的是统计求和计数的场景,场景比较单一
    而 AtomicLong 还具有 compareAndSet 等高级方法,可以应对除了加减之外的更复杂的需要 CAS 的场景。
    小结
    如果我们的场景仅仅是需要用到加和减操作的话,那么可以直接使用更高效的 LongAdder,但如果我们需要利用 CAS 比如 compareAndSet 等操作的话,就需要使用 AtomicLong 来完成。

     

    原子类 和 volatile的区别


    volatile只保证可见性
    volatile只保证了可见性,而不能保证原子性。例如主内存中volatile int a=0,在两个线程中时,如果都同时获取当前的volatile修饰的变量值,但是都执行自增操作,由于两个线程都在之前获取了值即a=0,两线程都执行自增后都是a=1了,这是两个线程同时更新主内存中的a,最后得到的结果是a=1,这个就和预期值不一样,由于并没有保证获取和赋值这个操作的原子性,会有线程安全问题。

    原子类保证了可见性和原子性
    上面的例子可以使用原子类AtomicInteger来修复,将自增操作换成incrementAndGet(),底层通过CPU指令保证原子性,解决线程安全问题

    使用场景

    volatile    通常情况下,volatile 可以用来修饰 boolean 类型的标记位,因为对于标记位来讲,直接的赋值操作本身就是具备原子性的,再加上 volatile 保证了可见性,那么就是线程安全的了
    原子类    对于先获取值,然后做一定修改,再赋值回去的操作,就需要原子类来保证原子性

    AtomicInteger 和 synchronized 的异同点


    相同点
    都能保证线程安全
    不同点
    AtomicInteger    使用CAS来保证线程的原子性    仅一个对象,少数场景使用    竞争在变量级别    乐观锁,低并发时性能好
    synchronized    使用monitor锁来保证线程安全性。再执行同步代码之前,需要先获取到monitor锁,执行完毕,释放锁。    既可以修饰一个方法,又可以修饰一段代码,可以根据我们的需要,非常灵活地去控制它的应用范围    通常会大于变量级别    悲观锁(jdk6之后锁升级优化后,低并发情况性能也不错),在高并发时由于AtomicInteger
     

    Java 8 中 Adder 和 Accumulator 有什么区别


    上面讲了Adder是通过CAS加分段思想来提高Atomic*的性能。

    而LongAccumulator是LongAdder的功能增强版,LongAccumulator在LongAdder只有数值加减的基础上提供自定义的函数操作。

    1. public class LongAccumulatorDemo {
    2. public static void main(String[] args) throws InterruptedException {
    3. LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0);
    4. ExecutorService executor = Executors.newFixedThreadPool(8);
    5. IntStream.range(1, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i)));
    6. Thread.sleep(2000);
    7. System.out.println(accumulator.getThenReset());
    8. }
    9. }

     

    上面是使用8线程数的线程池累加0-9。最后获取到结果为45.

    x是上一次的结果,y是传入的新值。由于使用多线程,当前的方法只适用于即使执行顺序不同,结果依然一样的情况,即交 换 性 \color{red}交换性交换性。

    下面几种场景适合使用:

    相加
    相乘
    最大值、最小值
     

  • 相关阅读:
    jvm 内存泄露、内存溢出、栈溢出区别
    怎么就敢用NodeJS写千万级别的服务后端
    (十一)Powershell调用java实验
    最新下载:MindMapper 17【软件附加安装教程】
    R 语言并行计算 spearman 相关系数,加快共现网络(co- occurrence network)构建速度
    被迫开始学习Typescript —— vue3的 props 与 interface
    非常详细的 Linux C/C++ 学习路线总结!助我拿下腾讯offer
    Apache SeaTunnel MongoDB CDC 使用指南
    docker安装
    分组背包问题
  • 原文地址:https://blog.csdn.net/weixin_53150299/article/details/133037446