图标
创作项目友邻自述归档留言

浅谈并发:CAS & AQS

前言

这篇文章躺在草稿里 6 个月了,最近才发现 2333,于是翻出来补全了下。

CAS

**CAS(Compare-and-Swap 比较和替换)**故名思意就是先比较然后替换的操作方式,是原子操作中的一种,同时也是无锁操作中的一种。在 Java 中广泛使用,常见的如 AQS 的更新状态、CopyOnWriteArrayList

通常情况下,我们操作某个共享资源,为了防止其他进程的干扰,我们通常会给这个共享资源加上一把锁,然而加锁,释放锁,竞争锁,线程的阻塞和唤醒是非常耗能的操作。而 CAS 采用的是记录内存位置和原始值,当记录的原始值与当前内存位置的值(当前值)相同的时候,就将新值更新到内存。如果不同说明在这过程中有其他线程干扰,执行失败策略。由于是无锁的操作,所以便没有了加锁和释放锁间一系列的额外开销,所以非常快。

流程

在进行 CAS 操作前需要三个操作参数:

  • 内存位置 V(它的值是我们想要去更新的)
  • 预期原值 A(上一次从内存中读取的值)
  • 新值 B(应该写入的新值)

操作过程:

先从 V 中取值,与 A 比较,如果相等则将 V 中的值更新成 B,如果不相等,说明 V 上的值被修改过了,也就是其他线程干扰了,这时候就不更新,进入失败的操作流程,比如重试,报错等。

缺点

当然如果 CAS 真的这么好用的话 Java 中早就放弃锁了。CAS 虽然很高效的解决原子操作,但是 CAS 仍然存在三大问题,同时也有使用场景的限制。

ABA 问题

原因:由于 CAS 是通过比较来判断是否进行更新操作,如果一个值从 A 变成 B,然后又被改成 A,在 CAS 检查时会认为值是没变的,实际上是变化了。

解决方法:为每次操作都加上版本号,如果版本号有变,即使值没变也说明是有变化的。

循环时间长开销大

原因:如果自旋长时间不成功,则会给 CPU 带来很大的执行开销。

解决方法:利用 pause 指令提升自旋性能。

只能保证一个值的原子操作

原因:如果有多个值需要同时操作,那么用 CAS 就无法简单解决了。

解决方案:可以对值进行合并,比如封装成一个对象等等。

使用场景限制

从 CAS 流程中我们可以看到,为了记录原始值,我们需要复制一份值,而这复制过程消耗的性能在少量的情况下并不明显,相比于锁更优。然而如果大量的执行 CAS 操作,或者操作的内容是比较庞大的,那么这些性能损耗就不能忽视了,较为极端的情况下甚至比锁还要差上很多。

AQS

AQS(AbstractQueuedSynchronizer 抽象排队同步器),是一个用来构建锁和同步器的框架,使用 AQS 能方便的构建出不同使用场景的同步器,常见的如 ReentrantLockCountDownLatch。AQS 采用模板方法模式,实现大量通用方法,子类通过继承方式实现其抽象方法来管理同步状态。

流程

当某个线程从 AQS 获取共享资源的时候,如果被请求的共享资源空闲,那么就将该线程设置为工作线程,然后锁定共享资源。如果被请求的共享资源被占用,那么就将线程加入 CLH 队列中,同时阻塞线程,进行排队等待。

CLH 队列

CLH 队列是一个虚拟的 FIFO 的双向队列。头节点是一个获取同步状态成功的节点。线程通过 AQS 获取锁失败,就会将线程封装成一个 Node 节点,插入队列尾。当有线程释放锁时,后唤醒头节点的 next 节点(第二个节点)尝试占用锁。

同步状态

在 AQS 中有一个 state 字段用于表示同步状态,使用了 volatile 关键字修饰,实现类可以通过 CAS 方式来更新同步状态。

资源共享模式

AQS 有两种资源共享模式:

  • 独占(Exclusive):即同一时间内只有一个线程能够获取共享资源。常见的实现如 ReentrantLock
  • 共享(Share):即一个共享资源可被多个线程同时使用。常见的实现如 CountDownLatch

AQS 为这两种资源共享模式提供了两组不同的方法,如独占式的 acquirerelease,共享式的 acquireSharedreleaseShared。按场景重写不同模式的方法即可实现不同的资源共享模式。当然也可以同时使用,比如 ReentrantReadWriteLock 就实现了独占和共享两种模式。

阻塞与唤醒

当有线程获取到共享资源了,其他线程获取时需要阻塞,当线程释放共享资源后,AQS 会负责唤醒在排队的线程。

AQS 通过 LockSupport 是用来创建锁和其他同步类的线程阻塞工具类,可以让线程在任意位置阻塞。每个使用 LockSupport 的线程都会与一个许可关联,如果该许可可用,则调用 park() 方法将会立即返回(加锁),否则会阻塞。如果许可尚不可用,则可以调用 unpark 使其可用(释放锁)。类似于 Object 类的 waitnotify

结语

ReentrantLock 相关的东西就暂时不再这篇文章写了,打算放到下一篇。

浅谈并发:CAS & AQS

https://blog.ixk.me/post/talking-about-concurrent-cas-and-aqs
  • 许可协议

    BY-NC-SA

  • 本文作者

    Otstar Lin

  • 发布于

    2021/02/13

转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!

浅谈并发:synchronized & ReentrantLock浅谈并发:ThreadLocal