CAS(Compare And Swap)

1. 乐观锁 - 悲观锁

悲观锁:以大家比较了解的sychronized为例子,其就是一个比较典型的悲观锁,悲观锁因为比较悲观,它认为在任何情况下都会天有不测风云,也就是说线程每次访问共享资源的时候都会出现冲突,所以必须先对数据进行上锁,从而保证对应的临界区资源在同一个时间段只能有一个资源去访问。

乐观锁:乐天派,它认为每次访问都不会出现对共享资源访问的冲突,所以在对共享资源访问的时候不会上锁,即多个线程均无需加锁,也无需等待。如果出现对共享资源访问的冲突,那么就会使用CAS的技术以确保线程执行的安全性。

从乐观锁的角度来看,其是一定不会出现死锁的现象的,因为其不上锁,所有线程都不会阻塞等待,永远的乐天派;悲观锁就相反了,逻辑处理不好就很容易出现死锁的现象。

悲观锁:多用于"写多读少",以避免共享资源修改冲突

乐观锁:多用于"读多写少",以提高效率。

以此引入CAS的技术,下面让我们来解开CAS神秘的面纱。

2. 什么是CAS?

2.1 简要概念

CAS(Compare And Swap)是一种硬件级别的原子操作,通过比较当前值(Var) 跟旧值也可以叫做预期值(Expect) 是否相等,如果相等,那么就将当前值修改为新值(New) ;反之则代表对应的资源被其他线程修改过,则放弃此次修改。但是这里的"放弃"并不是直接挂起,而是允许其再次进行尝试,尝试的过程依旧会占用CPU资源,自然,尝试也有一定的次数限制。 这其实就是自旋,即:

自旋是一种线程在竞争共享资源时不立即阻塞 ,而是通过循环(忙等待)反复尝试获取锁或执行操作 的机制。其核心目的是 避免线程挂起和唤醒的开销(如上下文切换、内核态切换),适用于 短时间等待 的场景。

a. CAS三要素

  • 当前值V(var)
  • 旧值 || 预期值(Expect)
  • 新值(New)

在上面已经大致说过了其运行的基本原理,就是通过比较当前值跟旧值是否相等,相等代表未被修改,将当前值V变为新值N,不等则不做修改,不断尝试,再次比较。

举一个例子:

现在在多线程的状态下,多个线程同时想要修改一个变量 S(代指Var) ,其原本的值为 S = 5(代指Expect) ,需要对其进行+1的操作,变成新值 6(代指New),其执行的流程如下:

首先,线程会比较当前值 S 跟旧值 5 是否一样,确保没有被修改,如果一样,将其更换为新值 6,这个时候 S的值就变成了6

如果发现S != 5 那么当前线程就会放弃本次修改操作,之后再尝试数次,次数用尽后自动放弃修改。

那么会不会出现说,我现在的S = 5,也就是跟旧值是一样的,但是准备修改的时候出现其他的线程将其修改为新值6了呢?不用担心,这种情况不会发生,因为CAS在底层是具有原子性的,要么同时成功,要么不成功。它是一种系统原语,是一条CPU指令,所以说底层是具有原子性的。

2,2 CAS优缺点

优点:

  • 无锁竞争,所以其并不会导致线程为了争抢共享资源而导致的线程阻塞,大大提高了线程的并发能力
  • 原子性,CAS所执行的每一条指令因为对应的都是底层的,有关操作系统的原语,所以具有较好的原子性,保证线程安全。

缺点:

  • 自旋开销:自旋所造成的CPU资源的浪费,尤其是在高并发的场景下
  • ABA问题:如果有线程将一个值,从A修改为B,之后再从B修改为A,那么在其他的线程看来,是没有发生任何修改的,也就是V跟E还是一样的,那么就会让多个线程均修改成功。
  • 单变量限制:CAS操作仅仅使用于对于单个变量的更新操作,不适用于涉及多个变量的复杂操作

2.3 CAS所面对的三个问题

a.ABA问题:

解决ABA问题,比较常见的方法就是使用版本号或者是时间戳用以进行记录,每次修改不仅修改对应的值,而且记录当下的时间戳,这样就保证了每次的原子性修改一定是跟之前的不一样的,从而解决ABA问题。

而在AtomicStampedReference当中,就设置了一个Pair内部类,用于记录时间戳,同时还使用native作了一个标记,使得这个静态变量具有可见性,如图:

b. 长时间自旋问题:

首先必须理清楚一个概念,为什么CAS看起来这样好,但是却只推荐在并发量比较小的时候使用?类似于sychnoized当中的轻量级锁,在高并发的场景下其依旧无法替代像mutex这样的基于互斥锁实现的功能?一个方面就是因为CAS的操作其实大部分都是自旋的,高并发情况下,可能有的线程它需要一段时间才能获取到对应的资源,那么在这一段时间内,其并没有阻塞,而是一直在运行,吃CPU资源,线程比较少还好,一旦躲起来,CPU负担可想而知。

也就是说,线程在自旋失败之后,还是在不简短的发起读请求,从而对CPU造成了比较大的资源浪费。针对于此,解决思路是让JVM支持硬件级处理器提供的pause指令。

pause指令能够让自旋失败的线程,先睡眠一段时间,之后再继续自旋。需要注意的一点是,在pause的这段时间里,此线程还是占有CPU的,不是像线程阻塞一样,直接放弃。睡眠的这一段时间,能够使得读操作的频率降低很多,提高效率。

c. 单变量限制

CAS对于单个变量能够保持原子性,但是共享变量一旦变成多个,那么就无法保证了,此时解决方法有两个

  • 使用AtomicReference保证对象之间具有原子性,把多个变量放到以恶个对象里面进行CAS操作,从而保证这些共享变量的原子性
  • 添加锁。锁内部的代码,只有当前的线程能够进行操作,其实跟上面那种方法类似,都是将这些共享变量看作是一个整体去操作了,从而确保原子性。

3. CAS在JAVA中的实现

在JAVA当中,CAS操作是由一个内部类Unsafe实现的,但是这个类是内部的,所以并不推荐直接使用。更具体的是通过**JNI(Java Native Interface)**实现的,如图所示:

通过这些操作实现CAS,确保其原子性。

在Java当中,可以使用并发包Atomic这些类,他们封装了CAS操作,提供了线程安全的原子操作。

相关推荐
zhang238390615430 分钟前
IDEA add gitlab account 提示
java·gitlab·intellij-idea·idea
牛马baby1 小时前
Java高频面试之并发编程-07
java·开发语言·面试
卓怡学长1 小时前
w304基于HTML5的民谣网站的设计与实现
java·前端·数据库·spring boot·spring·html5
YONG823_API1 小时前
深度探究获取淘宝商品数据的途径|API接口|批量自动化采集商品数据
java·前端·自动化
yzhSWJ1 小时前
Spring Boot中自定义404异常处理问题学习笔记
java·javascript
盖世英雄酱581362 小时前
分布式ID所有生成方案
java·后端
敖云岚2 小时前
【AI】SpringAI 第五弹:接入千帆大模型
java·大数据·人工智能·spring boot·后端
桦说编程2 小时前
CompletableFuture典型错误 -- 代码出自某大厂
java·后端·响应式编程