JavaEE之CAS

上文我们认识了许许多多的锁,此篇我们的CAS就是从上文的锁策略开展的新概念,我们来一探究竟吧

1. 什么是CAS?

CAS: 全称Compare and swap,字⾯意思:"比较并交换",⼀个CAS涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较A与V是否相等。(比较)
  2. 如果比较相等,将B写入V。(交换)
  3. 返回操作是否成功。

CAS伪代码

下面写的代码不是原子的,真实的CAS是⼀个原子的硬件指令完成的.这个伪代码只是辅助理解CAS的工作流程.

java 复制代码
while{
 boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
 &address = swapValue;
 return true;
 }
 return false;
 }
}

说明:

  1. address: 表示要修改的内存地址
  2. expectValue: 预期值
  3. swapValue:要设置的新值

执行流程:

  1. 用一个预期值去和内存中的值做比较
  2. 如果预期值与内存中的值相等,就用新的值更新内存中的值
  3. 如果预期值与内存中的值不相等,就用进入下一次比较

我们前面学习过的线程安全问题是由于CPU的随机调度导致的,我们的处理方法是给有关修改共享变量的代码块加了synchronize关键字,保证了原子性,解决了线程安全问题,

那么问题来了,
为什么我们看到的CAS伪代码并没有保证原子性,那么他是如何保证原子性的呢??

我们从一段例子来讲解:

总结:

  1. CAS对应的是硬件指令cmpxchg从CPU做了原子性支持,与LOCK 、UNLOCK 指令同地位
  2. CAS操作每次读取(LOAD)数据是从主内存中读取,并没有对应的工作内存
  3. 自旋锁是通过while把CAS进行包裹,让CAS没有成功的时候不停的执行,直到成功执行为止
  4. CAS是一个真正的指令,JVM调用了本地方法之后,对应的CAS指令会在CPU上执行
    执行中LOAD数据会访问主内存

2. CAS实现自旋锁

基于CAS实现更灵活的锁,获取到更多的控制权.
自旋锁伪代码:

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;
 }

3. JDK中基于CAS实现的原子类

说明:

  1. JDK中提供了CAS实现的原子类,我们举其中一种AtomicInteger类来说明
  2. 我们调用其中的方法getAndIncrement()
java 复制代码
//自增 ++i
atomicInteger.getAndIncrement();
  1. 我们发现getAndIncrement()方法本质是getAndIncrement()
java 复制代码
public final int getAndIncrement() {
        return U.getAndAddInt(this, VALUE, 1);
}
  1. 其中调用了Unsafe这个类的方法,是JDK内部的工具类,不建议外部程序员直接调用
java 复制代码
private static final Unsafe U = Unsafe.getUnsafe();
//获取内存中字段在对象中的偏移地址
    private static final long VALUE
        = U.objectFieldOffset(AtomicInteger.class, "value");
java 复制代码
//1. o表示原子类对象
//2. offset表示内存中字段在对象中的偏移地址
//3. delta表示传入的值
public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
        //v是CAS操作之前获取的预期值
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        //while不停的自旋
        return v;
    }
java 复制代码
   public final boolean weakCompareAndSetInt(Object o, long offset,//内存地址
                                              int expected,//预期值
                                              int x) {//要设置的值
        return compareAndSetInt(o, offset, expected, x);//最后调用compareAndSetInt()方法
    }
java 复制代码
//native表示本地方法,即对应了一条CAS指令cmpxchg
public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);
  1. 图解如下:

4. CAS的ABA问题

首先我们要知道什么是ABA问题???

ABA的问题:

假设存在两个线程t1和t2.有一个共享变量value,初始值为A.

接下来,线程t1想使用CAS把value值改成Z,那么就需要

先读取value的值,记录到oldValue变量中.

使用CAS判定当前value的值是否为A,如果为A,就修改成Z.

但是,在t1执行这两个操作之间,t2线程可能把value的值从A改成了B,又从B改成了A

举个例子: 今天是疯狂星期四,我和我的女友共用一个小荷包吃饭,我和她说如果小荷包里面有100元 (A状态) 晚上6点就点KFC吃,我怕她忘记了,我就先自己点了,此时,小荷包余额为50元 (B状态) 然后我的朋友疯狂星期四V我50,小荷包的钱又变回了100 (A状态),她再点的时候,发现小荷包还是100元,她就再点了一份,就导致多点了一份KFC

那么我们该如何解决ABA问题呢??

在点KFC的例子当中,我们可以让他查看一下消费记录(版本号),此时的100元是否是开始的100元,

由此可得,我们可以可以给value数据加上一个版本号,当线程2要修改时,不仅要value要符合预期值,同时版本号也要符合
即:

CAS操作在读取旧值的同时,也要读取版本号.

真正修改的时候,

  • 如果当前版本号和读到的版本号相同,则修改数据,并把版本号+1.
  • 如果当前版本号高于读到的版本号.就操作失败(认为数据已经被修改过了).

版本号可以用自增的数值,也可以用随机的UUID,也可以使用时间戳

java 复制代码
System.out.println(UUID.randomUUID());

生成随机数:

相关推荐
青木川崎1 分钟前
Mybatis面试题
java·开发语言·mybatis
m0_748255262 分钟前
记录 idea 启动 tomcat 控制台输出乱码问题解决
java·tomcat·intellij-idea
_半夏曲3 分钟前
WildFly与tomcat的异同
java·tomcat·wildfly
wclass-zhengge8 分钟前
02UML图(D1_结构图)
java·开发语言·算法
孑么15 分钟前
力扣 打家劫舍
java·算法·leetcode·职场和发展·动态规划
java1565505797018 分钟前
大模型推理:vllm多机多卡分布式本地部署
java
菜鸟阿康学习编程22 分钟前
JDBC 实战项目(增删改查小系统,接近完美!)017
java·开发语言·数据库
007php0071 小时前
深入了解计算机网络中的路由协议与性能优化
java·开发语言·数据库·后端·python·计算机网络·golang
秋野酱1 小时前
基于SpringBoot+Vue旅游管理系统的设计和实现(源码+文档+部署讲解)
java
落霞与孤鹭齐飞。。1 小时前
SSM旅游信息管理系统
java·开发语言·mysql·毕业设计·课程设计·旅游