JavaEE:CAS详解

一.什么是CAS

CAS: 全称 Compare and swap ,字面意思 :" 比较并交换 " ,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
我们来进行操作:

  1. 比较 V 和 A 是否相等。(比较)
  2. 如果比较是相等的,将B 写入 V。(交换)
  3. 返回操作是否成功。
    伪代码如下:
java 复制代码
boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

这个就大概介绍了CAS的工作流程

二.CAS是如何实现的

针对不同的操作系统,JVM用到了不同的CAS实现原理

  • java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。

三.CAS的应用

1.实现原子类

我们都知道,在多线程环境下,我们的++操作不是线程安全的。那么我们的CAS就自己将 ++ 操作是现成了原子类。

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class CAS {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        int ret = atomicInteger.incrementAndGet();
        System.out.println(ret);
    }
}

此时在Java中,为我们提供了这样的 ++ 原子类操作。以上代码的运行结果为 1 。

那么我们深究一下这其中的实现原理

先来看下面的伪代码:

java 复制代码
class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

首先要知道我们的CAS操作时原子的。在多线程环境下,那么他是如何保证每次读到的值,都是我们预期的值呢?

我们看下面的图片:

🎈第一步

两个线程都读取 value 的值到 oldValue 中. (oldValue 是一个局部变量, 在栈上. 每个线程有自己的栈)

🎈第二步

线程1进行CAS操作,先进行比较,发现 oldValue 和 Value 都是相同的,然后进行oldValue+1操作,最后再写入到内存中。此时主内存中Value的值为 1

🎈第三步

此时线程2进行操作。再进行 比较 oldValue 和 Value 是否相同的时候,发现不相同。此时重新进入while循环!在循环的时候, 会刷新oldValue的值-->1,此时oldValued 和 Value的值就相同了。正常进行+1赋值操作。

🎈第四步

线程 1 和 线程 2 返回各自的 oldValue 的值即可
我们可以很清楚的看到,CAS使用while循环巧妙的解决了++操作的线程安全问题。

2.实现自旋锁

先来看自旋锁的伪代码:

java 复制代码
public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

自旋锁其实时一种锁的策略,它的实现原理就是如果当前的线程是有锁的,那么就一直while循环尝试获取锁,如果没锁了,那么就直接可以获取到CPU了。

四.CAS的ABA问题

我们先来看个例子:

假如我要去银行里取钱,我的存款是100元,我期望的是取出50元。

那么取款机创建了两个线程,假如线程1执行了-50的操作,线程2再执行的时候会报错。

1.正常的过程

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期
    望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
  3. 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.

2.异常的过程

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期
    望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
  3. 在线程2 执行之前, 我的朋友正好给滑稽转账 50, 账户余额变成 100 !!
  4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行了扣款操作!
    那么此时就执行了两次扣款操作,这显然不是我们所期望的。

3.解决办法

给要修改的值 , 引入版本号 . 在 CAS 比较数据当前值和旧值的同时 , 也要比较版本号是否符合预期
版本号随着每次的使用而进行更新 。
还是刚才银行取钱的案例
为了解决 ABA 问题, 给余额搭配一个版本号 , 初始设为 1.

  1. 存款 100. 线程1 获取到 存款值为 100, 版本号为 1, 期望更新为 50; 线程2 获取到存款值为 100,
    版本号为 1, 期望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50, 版本号改为2. 线程2 阻塞等待中.
  3. 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100, 版本号变成3.
  4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读
    到的版本号为 1, 版本小于当前版本, 认为操作失败.
    此时引入了版本号,ABA问题就完美的得到了解决。

总结:CAS是面试中常考的问题,我们需要深度学习,并且理解其中的操作机制。

相关推荐
小灰灰__10 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭13 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果34 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林39 分钟前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨1 小时前
El表达式和JSTL
java·el
duration~2 小时前
Maven随笔
java·maven
zmgst2 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD2 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp2 小时前
Java:数据结构-枚举
java·开发语言·数据结构
暗黑起源喵2 小时前
设计模式-工厂设计模式
java·开发语言·设计模式