Java 多线程之CAS,Unsafe和原子类详解

254 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

CAS

CAS 的全称为 Compare-And-Swap,直译就是对比交换

是一条 CPU 的原子指令,其作用是让 CPU 先进行比较两个值是否相等,然后原子地更新某个位置的值,CAS 是靠硬件实现的,JVM 只是封装了汇编调用,那些 AtomicInteger 类便是使用了这些封装后的接口

简单来说,CAS 操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换

如果不使用 CAS,在高并发下,多线程同时修改一个变量的值我们需要 synchronized 加锁(可能有人说可以用 Lock 加锁,Lock 底层的 AQS 也是基于 CAS 进行获取锁的)

java 中为我们提供了 AtomicInteger 原子类(底层基于 CAS 进行更新数据的),不需要加锁就在多线程并发场景下实现数据的一致性

CAS 会导致的问题

CAS 方式为乐观锁,synchronized 为悲观锁。因此使用 CAS 解决并发问题通常情况下性能更优,但使用 CAS 方式也会有几个问题

ABA

如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过

J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效

循环时间开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就需要用锁了

从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作

UnSafe 类

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用

但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重

这个类尽管里面的方法都是 public 的,但是并没有办法使用它们,JDK API 文档也没有提供任何关于这个类的方法的解释。总而言之,对于 Unsafe 类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然 JDK 库里面的类是可以随意使用的

image.png

如上图所示,Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类

原子类

原子更新基本类型

  • AtomicBoolean:原子更新布尔类型
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新长整型

原子更新数组类型

  • AtomicIntegerArray: 原子更新整型数组里的元素。
  • AtomicLongArray: 原子更新长整型数组里的元素。
  • AtomicReferenceArray: 原子更新引用类型数组里的元素

这三个类的最常用的方法是如下两个方法:

  1. get(int index):获取索引为index的元素值。
  2. compareAndSet(int i,E expect,E update): 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值

原子更新引用类型

Atomic包提供了以下三个类:

  • AtomicReference:原子更新引用类型
  • AtomicStampedReference:原子更新引用类型, 内部使用Pair来存储元素值及其版本号
  • AtomicMarkableReferce:原子更新带有标记位的引用类型

原子更新字段类

Atomic包提供了四个类进行原子字段更新:

  • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器
  • AtomicLongFieldUpdater: 原子更新长整型字段的更新器
  • AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型
  • AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述

原子类 AtomicInteger 详解

以 AtomicInteger 为例,常用 API:

public final int get():获取当前的值
public final int getAndSet(int newValue):获取当前的值,并设置新的值
public final int getAndIncrement():获取当前的值,并自增
public final int getAndDecrement():获取当前的值,并自减
public final int getAndAdd(int delta):获取当前的值,并加上预期的值
void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

安全的自增:

private AtomicInteger count = new AtomicInteger();
public void increment() {
    count.incrementAndGet();
}
// 使用 AtomicInteger 后,不需要加锁,也可以实现线程安全
public int getCount() {
    return count.get();
}

AtomicInteger 底层用的是volatile的变量和CAS来进行更改数据的

  • volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值
  • CAS 保证数据更新的原子性

使用原子类解决ABA的问题

可以使用 AtomicStampedReference(维护一个版本号)、AtomicMarkableReference(维护一个boolean值) 来解决 ABA 的问题