高并发研究室06-原子类Atomic*

本章主要讲一下原子类

原子类定义

在了解原子类,我们先需要知道原子性。在程序中,原子性指的是一组操作要么全都操作成功,要么全都失败,不能只操作成功其中的一部分。

原子类指的就是在java.util.concurrent.atomic 下的类,就是具有原子性的类,可以原子性地执行添加、递增、递减等操作。比如常见的线程不安全的i++,可以替换成getAndIncrement

原子类的作用

原子类是为了保证并发情况线程安全。比锁更具有一些优势,可以理解为乐观锁。

  • 粒度更细:原子变量可以把竞争范围缩小到变量级别,通常情况下,锁的粒度都要大于原子变量的粒度。例如synchronized 锁的是代码块或者方法。
  • 效率更高:除了高度竞争的情况之外,使用原子类的效率通常会比使用同步互斥锁的效率更高,因为原子类底层利用了 CAS 操作,不会阻塞线程。

原子类概览

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

Atomic\ 基本类型原子类

介绍一个常用的AtomicInteger。它是对于 int 类型的封装,并且提供了原子性的访问和更新。也就是说,我们如果需要一个整型的变量,并且这个变量会被运用在并发场景之下,我们可以不用基本类型 int,也不使用包装类型 Integer,而是直接使用 AtomicInteger,这样一来就自动具备了原子能力,使用起来非常方便。

常用的一些方法有:

  • public final int get() //获取当前的值
  • public final int getAndSet(int newValue) //获取当前的值,并设置新的值
  • public final int getAndIncrement() //获取当前的值,并自增
  • public final int getAndDecrement() //获取当前的值,并自减
  • public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
  • boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值更新为输入值(update)这个方法也是 CAS 的一个重要体现。

Array 数组类型原子类

  • AtomicIntegerArray:整形数组原子类;
  • AtomicLongArray:长整形数组原子类;
  • AtomicReferenceArray :引用类型数组原子类。

Atomic\Reference 引用类型原子类

AtomicReference 类的作用和AtomicInteger 并没有本质区别, AtomicInteger 可以让一个整数保证原子性,而AtomicReference 可以让一个对象保证原子性。这样一来,AtomicReference 的能力明显比 AtomicInteger 强,因为一个对象里可以包含很多属性。 这个类别之下,除了 AtomicReference 之外,还有:

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

ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过,这与乐观锁的设计思想不符合。ABA问题的解决思路是,每次变量更新的时候把变量的版本号加1,那么A-B-A就会变成A1-B2-A3,只要变量被某一线程修改过,改变量对应的版本号就会发生递增变化,从而解决了ABA问题。

Atomic\FieldUpdater 原子更新器

  • AtomicIntegerFieldUpdater:原子更新整型的更新器;
  • AtomicLongFieldUpdater:原子更新长整型的更新器;
  • AtomicReferenceFieldUpdater:原子更新引用的更新器。

就是将一个不具备原子性的类更新成原子类

Adder 加法器 与 Accumulator 积累器

Addr加法器,分别是 LongAdder 和 DoubleAdder。

Accumulator 积累器,分别是 LongAccumulator 和 DoubleAccumulator。

Adder 和 Accumulator 都是 Java 8 引入的,是相对比较新的类。对于 Adder 而言,比如最典型的 LongAdder,我们在第 40 讲的时候已经讲解过了,在高并发下 LongAdder 比 AtomicLong 效率更高,因为对于 AtomicLong 而言,它只适合用于低并发场景,否则在高并发的场景下,由于 CAS 的冲突概率大,会导致经常自旋,影响整体效率。

而 LongAdder 引入了分段锁的概念,当竞争不激烈的时候,所有线程都是通过 CAS 对同一个 Base 变量进行修改,但是当竞争激烈的时候,LongAdder 会把不同线程对应到不同的 Cell 上进行修改,降低了冲突的概率,从而提高了并发性。

Accumulator 和 Adder 非常相似,实际上 Accumulator 就是一个更通用版本的 Adder,比如 LongAccumulator 是 LongAdder 的功能增强版,因为 LongAdder 的 API 只有对数值的加减,而 LongAccumulator 提供了自定义的函数操作。

volatile 与 原子类的区别

首先先了解volatile,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。同时会禁止指令重排序( 指令的执行顺序并不一定会像我们编写的顺序那样执行,为了保证执行上的效率,JVM可能会对指令进行重排序),保证了并发的有序性。一般结合synchronized和lock来保障原子性

所有线程的共享变量都会存储在主内存(方法区,堆)中,每个线程都有自己的独有工作区(栈)。如果对共享变量加上volatile关键字修饰的话,它可以保证当A线程对变量i值做了变动之后,会立即刷回到主内存中,而其它线程读取到该变量的值也作废,强迫重新从主内存中读取该变量的值,这样在任何时刻,AB线程总是会看到变量i的同一个值。

很多人在中断线程时会采用volatile这种标记方法。

总结:

volatile 和原子类的使用场景是不一样的,如果我们有一个可见性问题,那么可以使用 volatile 关键字,但如果我们的问题是一个组合操作,需要用同步来解决原子性问题的话,那么可以使用原子变量,而不能使用 volatile 关键字。

通常情况下,volatile 可以用来修饰 boolean 类型的标记位,因为对于标记位来讲,直接的赋值操作本身就是具备原子性的,再加上 volatile 保证了可见性,那么就是线程安全的了。

而对于会被多个线程同时操作的计数器 Counter 的场景,这种场景的一个典型特点就是,它不仅仅是一个简单的赋值操作,而是需要先读取当前的值,然后在此基础上进行一定的修改,再把它给赋值回去。这样一来,我们的 volatile 就不足以保证这种情况的线程安全了。我们需要使用原子类来保证线程安全。

本文使用 mdnice 排版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值