介绍了原子操作类,以
AtomicLong
介绍原子类的实现,并对原子类的分类和使用(基本数据类型、引用类型、对象属性更新器、数组、累加器)进行说明。
简介
JUC中对于简单的原子性问题提炼封装成一系列的原子类,这是一种无锁方案。相对于互斥锁而言,其最大的好处早会性能,互斥锁为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性。
原子类使用到了硬件的支持,CPU为了解决并发问题,提供了CAS (Compare And Swap)
指令,CAS 指令包含三个参数:共享变量的内存地址 A、用于比较的值 B 和 共享变量的新值 C。只有内存地址中 A 处的值等于 B 时,才能将内存地址 A 处的值更新为新值 C。且作为一条 CPU 指令,CAS 本身能够保证原子性。
当然,CAS 不是完美无缺的。CAS 方案会有 ABA 问题,即值被更新后,后又被更新为原值。是否需要关系 ABA 问题也需要依照特定情况而言,可以使用版本号标识解决 ABA 问题,后续会进行介绍。
实现
Atomic包里的类基本都是使用Unsafe
实现的包装类。接下来以AtomicLong
的方式先介绍典型实现。
1 | public class AtomicLong extends Number implements java.io.Serializable { |
上述getAndAddLong
方法就是 CAS 经典使用案例。可以看出,使用 CAS 来解决并发问题一般都会伴随着自旋。一般的无锁代码经常会采用如下的方式,使用的是compareAndSet
方法。
1 | do { |
分类
原子类可以按照以下的方式进行划分:
类型 | 具体类 |
---|---|
基本数据类型 | AtomicBoolean、AtomicInteger、AtomicLong |
引用类型 | AtomicReference、AtomicStampedReference、AtomicMarkableReference |
对象属性更新器 | AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater |
数组 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray |
累加器 | DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder |
AtomicIntegerArray
将数组value传递给构造方法,然后会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。
1 | public static void main(String[] args) { |
原子更新基本类型只能更新一个变量,如果要原子更新多个变量,就需要使用原子更新引用类型提供的类,以AtomicReference
举例:
1 | public static void main(String[] args) { |
上文提到解决ABA问题可以使用版本号,只要保证版本号是递增的,那么即便 A 变成 B 之后再变回 A,版本号也不会变回来。AtomicStampedReference
实现的 CAS 方法就增加了版本号参数,方法签名如下:
1 | boolean compareAndSet( |
1 | public static void main(String[] args) { |
AtomicMarkableReference
的实现机制则更简单,将版本号简化成了一个 Boolean 值,方法签名如下:
1 | boolean compareAndSet( |
如果需要原子地更新某个类里的某个字段时,可以使用原子更新字段类,以AtomicIntegerFieldUpdater
举例:
1 | public static void main(String[] args) { |
DoubleAccumulator
、DoubleAdder
、LongAccumulator
和 LongAdder
,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。JDK8 之所以会新增这些类,是因为:原子类在高并发下大量线程会同时取竞争更新同一个原子变量,但由于只有一个线程 CAS 成功,就会造成大量线程竞争失败,会通过无限循环不断进行自旋尝试 CAS 操作,而这会浪费CPU资源。
以 LongAdder 为例,既然 AtomicLong 性能瓶颈在于多线程竞争一个变量的更新而产生的,那就把一个变量分解为多个变量,让多线程竞争多个资源。LongAdder 就是以这种方式进行设计的,其内部维护着一个延迟初始化的原子性更新数组(默认情况下Cell 数组是null)和一个基值变量base:
- 其内部维护多个 Cell 变量,每个 Cell 有一个初始值为 0 的 long 类型变量,同等并发量的情况下争夺单个变量的线程量会减少。
- 此外,多个线程争夺同一个 Cell 失败后并不会在当前在当前 Cell 变量上一直自旋 CAS 重试,而是尝试在其他 Cell 的变量上进行 CAS 尝试,以增加当前线程重试 CAS 成功的概率。
- 当最后获取累加值时就是将所有 Cell 变量的 value 值累加后加上 base 后返回的。
- 原子性数组元素的内存地址是连续的,所以数组内的多个元素能经常共享缓存行,因此使用
@sun.misc.Contended
注解对 Cell 类进行字节填充,以避免数组中多个元素共享缓存行(避免伪共享)。
关于伪共享
为了解决CPU和主内存间的速度差异,之间会添加一级或多级告诉缓冲存储器(Cache),而 Cache 内部是按行存储的,每行称为一个 Cache 行,Cache 行的大小一般为 2 的幂次数字节,这是 Cache 与主内存进行数据交换的单位。
当CPU访问变量不存在时,先去 Cache 中看存不存在,没有的话就去内存取,然后将该变量所在内存区域(大小相当于 Cache 行大小)复制到 Cache 中,注意不是复制单个变量而是复制内存块。当多个线程同时修改一个缓存行里面的多个变量时,由于每时刻只能有一个线程操作缓存行,所以相对于将每个变量放到单独缓存行中,性能会有所下降,这就是伪共享。
当两个不同线程同时进行操作时,线程1使用CPU1更新变量x,那么在缓存一致性协议下,CPU2中变量x对应的缓存行会失效,线程2想要对与x在同一缓存行中的y进行操作时,就只能去下一级缓存中寻找。
一般通过字节填充的方式解决伪共享,JDK8 中提供
@sun.misc.Contended
注解就可解决伪共享,可用于修饰类,也可以修饰变量。此注解只能用于 rt 包下的核心类,用户类路径下想使用,需要添加 JVM 参数:-XX:-RestrictContended
。填充的宽度默认为128 ,要自定义宽度则可以设置-XX:ContendedPaddingWidth
参数。 (from 《Java并发编程之美》)
Reference
https://time.geekbang.org/column/article/90515
《Java并发编程的艺术》
《Java并发编程之美》