CAS

概念

比较并替换,是一种实现并发算法时常用到的技术,Java 并发包中的很多类都使用的 CAS 技术。(比如: AtomicXXX 系列);
CAS是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。

==自增操作不是原子操作==

简答理解

CAS 是一种无锁算法,CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。CAS 指令执行时,当且仅当内存地址 V 的值与预期值 A(旧的值)相等,才将内存地址 V 的值修改为 B(更新的值),否则就什么都不做。比较替换的操作是原子操作。

图解

因为 t1 和 t2 线程都同时去访问同一变量 56,所以他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以 t1 和 t2 线程的预期值都为 56。

假设 t1 在与 t2 线程竞争中线程 t1 能去更新变量的值,而其他线程都失败。(失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试)。t1 线程去更新变量值改为 57,然后写到内存中。此时对于 t2 来说,内存值变为了 57,与预期值 56 不一致,就操作失败了(想改的值不再是原来的值)。

上图通俗的解释是:CPU 去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。

就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作(拷贝主内存的数据到自己的工作内存空间…)。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的 commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。

理解

  1. 首先 CPU1 和 CPU2 从内存中读值–>A,CPU2 进行了 +1 操作成功。
  2. CPU1 一系列操作后想对 A+2,就将 A 和 A+2 传到中间过程 。
  3. 中间过程就是循环语句的判断:判断现在内存中的 A 和传过来的旧 A 值一样不,一样就修改内存中的数据,返回 true,循环终止;
  4. 不一样,返回 false,循环为 true,循环继续,A 获取最新的内存的值(内存可见性操作:使用了 volatile)。
  5. 获取完之后再去将新的 A 值和新的 A 值 +2 传到中间过程。(这里就能看出为啥要一直循环了,因为如果 A 值不同那旧的 A 值 +2 会导致数据错误)。
  6. 就这样循环下去,直到成功为止。
  7. ==一句话==:旧值和当前内存中的值一样,修改,不一样,使用内存中的值替换旧值,继续下一次的比较,直到修改成功为止。

缺点

  1. 循环时间长,开销大:我们可以看到 getAndInt 方法执行时,==如果 CAS 失败,会一直进行尝试(在这个过程中预期值 A 一直在更新)==,如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。
  2. 只能保证一个共享变量的原子操作:当对一个共享变量操作时,我们可以用循环 CAS 的方式来保证原子操作,但对多个共享变量操作时, 循环 CAS 就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
  3. 什么是 ABA 问题?怎么解决 ABA 问题
    1. 如果内存地址 V 初次读取的值是 A,并且在准备赋值的时候检查到它的值仍然为 A,那么我们就能说它的值没有被其他线程改变过了吗?
    2. 如果这段期间内它的值曾经被改成了 X,后来又被改回了 A,那 CAS 操作就会误认为它从来没有被改变过,这个漏洞被称为 CAS 操作的 “ABA”问题。Java 并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量的值的版本来保证 CAS 的正确性。因此,==在使用 CAS 前要考虑清楚 ABA 问题是否会影响程序并发的正确性== ,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效

AQS

抽象的队列同步器, 用来构建锁或者其他同步器组件的重量级基础框架及整个 JUC 体系的基石,通过内置的 FIFO 队列来完成资源获取线程的排队工作,并通过一个 int 类型变量表示持有锁的状态。

AQS的底层也是基于CAS获取锁的。

解释下 AQS 翻译后的意思

  • 抽象: 符合模板设计模式,作为核心父类给子类继承
  • 队列: 对抢占不到锁的管理
  • 同步器: 队列的排队的线程进行管理

AQS 作用

加锁会导致阻塞,有阻塞就会排队,实现排队必然需要有某种形式的队列来进行管理

AQS 的原理

AQS 为实现阻塞锁,依赖先进先出的一个等待依靠一个原子 int 值来表示状态,通过占用和释放方法,改变状态值 AQS 使用一个 volatile 的 int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来表示完成获取资源的排队工作。

将每条要去抢占资源的线程封装成一个 Node 节点来实现锁的分配,通过 CAS 完成对 State 值得修改

AQS 的变量:

  1. state 变量: 判断是否阻塞
    • 阻塞需要排队(前提:自旋如果达到一定时间),实现排队必须需要队列
  2. Node 节点的变量
    • 队列中每个队列的个体,也就是 Node

参考: