文章目录

  • 概念
  • CAS的特性:乐观锁
  • CAS的自旋等待
  • CAS 的缺点
  • ABA 问题

在多线程并发当中,要保证线程的原子性。

原子类的原子性是通过 volatile + CAS 来实现原子操作的。

概念

CAS(Compare And Swap)是指比较并交换。CAS算法 CAS(V,E,N)包含有3参数,V 表示要更新的变量,E 表示预期的值,N 表示新值。

在且仅在 V 值等于 E 值时,才会将 V 值设为 N, 如果V 值和E 值不同,则说明已经有其他线程做了更新,当前线程什么都不做。最后,CAS返回当前 V 的真实值。

这种做法的效率是高于加锁的,当 V 和E 判断不一样的时候,也就不更新值,也不会发送阻塞,继续获得CPU的执行权,继续判断并执行。

CAS的特性:乐观锁

CAS 的操作采用的了乐观锁的思想,总是认为自己是可以成功完成操作的。在有多个线程可以同时使用 CAS 操作一个变量时,只有一个会胜出并成功更新,其余均会失败。
失败的线程是不会挂起的,仅仅是被允许再次尝试,当然,也允许失败的线程放弃操作的。 基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

CAS的自旋等待

在 JDK 的原子包 java.util.concurrent.atomic 的里面提供了一组原子类,这些原子类的基本特征就是在多线程的环境下,在有多个线程同时执行这些类的实例包含的方法时,会有排他性。其内部便是基于 CAS 算法来实现的,即在某个线程进入方法中执行其中的指令时,不会被其他线程锁打断;而别的线程就像自旋锁一样,(就是在不停的旋转重试当中不断尝试)一直等到该方法执行完成才由 JVM 从等到的队列中选择从一个线程进入。

相对于 synchronized 阻塞算法, CAS 是非阻塞算法的一种常见的实现。 由于 CPU 上下文的切换比 CPU 指令集的操作更加耗时,所以 CAS 的自旋操作在性能上有了很大的提升。

CAS 的缺点

CAS 使用的是自旋锁的方式,由于该锁是会不断循环判断的,因此是不会类型synchronize 线程阻塞导致线程切换的。
但是在不断自旋的过程当中,会导致CPU 的消耗,特别在大并发的情况下容易导致 CPU 跑满。


在使用 CAS 算法的实现有一个重要的前提:是需要取出内存中某个时刻的数据,然后在一时刻就立即进行了比较、替换,在这个时间差内可能数据已经发生了变换,就产生了 ABA 问题。

ABA 问题

ABA 问题就是指第一个线程从内存 V 位置取出 A ,这时第二个线程也从内存中取出 A 并将 V 位置的数据先修改为 B ,接着又将 V 位置的数据修改为 A ,这时第1个线程再进行 CAS 操作时,就会发生在内存中的任然是 A ,然后第一个线程也能成功操作了。

尽管从线程一的角度来看,CAS 的操作是成功的,但在该过程当中其实 V 位置的数据发生了变化,只是在第一个线程当中,没有感受到罢了,这在某些应用场景下是可能出现过程数据不一致的问题。

所以为了解决这个 ABA 的问题,在部分乐观锁当中,是通过版本号(version)来解决 ABA 问题的。

具体的操作:在乐观锁每次在执行数据的修改操作的时都会带上一个版本号,在预期的版本号和数据的版本号是一致时就可以执行修改操作,并对版本号执行加 1 的操作,否则就执行失败。因此在每次操作的版本号都会随之增加,所以就不会出现 ABA 问题。因为版本号只会增加不会减少。

代码演示:

在这里线程二里面,是不应该改变的,在这里就造成了 ABA 的问题,所以在这里我们就要去解决这个 ABA 的问题。


上一篇: ===》 Java并发中的锁机制