CAS详解

什么是CAS

CAS其实就是compareAndSet比较并交换。
假设两个线程t1和t2修改主物理内存,t1期望值和真实值一样,那就修改,t2线程发现主物理内存和期望值不一样,那么修改失败,需要重新获取主物理内存真实值。关于这个CAS你需要了解volatile,volatile详解已经详细介绍了。
下面我们用代码做个试验:

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {
    /**
     * 什么是CAS == compareAndSet
     * 1.比较并交换
     * @param args
     */
    public static void main(String[] args){
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,2019)+"\t current data:"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5,2018)+"\t current data:"+atomicInteger.get());
    }
}

运行结果:
在这里插入图片描述
下面我们来讲究下底层原理,CAS为什么保证原子性?原因是unsafe类。

Unsafe类

Unsafe类是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于sum.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。
Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都可以直接调用操作系统底层资源执行相应任务.

我们读下源码atomicInteger.getAndIncrement();方法来研究下

  1. 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
    图中的i就是加1操作:
    在这里插入图片描述
  2. 变量value用volatile修饰,保证了多线程之间的内存可见性。
    在这里插入图片描述
  3. CAS
    我们继续解读源码如图:
    CAS的全称为Compare-And-Swap,它是一条CPU并发原句
    它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
    CAS并发原语体现在java语言中就是sun.misc.Unsafe类的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题
    在这里插入图片描述
  4. CAS缺点
    缺点一
    循环时间长开销很大
    如果CAS失败,会一直进行尝试,如果CAS长时间不成功,肯能会给CPU带来很大的开销。
    缺点二
    只能保证一个共享变量的原子操作。
    缺点三
    引出来ABA问题。

什么是ABA

ABA借用一句话,就是狸猫换太子。
首先有2个线程t1,t2主物理内存值为A,t1,t2都把主物理内存A 拷贝到自己的工作空间,t1执行时间需要5秒,t2执行时间需要1秒,t2执行快所以t2把A改为B写回给主内存,然后在把B写成A,期间这个操作是多次,当t1线程CAS比较时发现期望值和主内存值(真实值)一样的都是A,那么t1把自己修改的写回给主内存,但是t2多次修改了主内存,t1是不知道的,这就是ABA问题,CAS的ABA问题是只管开头和尾巴,中间过程是不管的。
如何解决ABA问题呢?下面我们来介绍:
原子引用
我们可以使用包装类java.util.concurrent.atomic.AtomicReference来保证数据一致性。
代码演示:

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import java.util.concurrent.atomic.AtomicReference;

@Getter
@ToString
@AllArgsConstructor
class User{
    String userName;
    int age;
}
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User user1 = new User("张三",15);
        User user2 = new User("李四",20);
        //原子引用包装类
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(user1);

        System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get().toString());
    }
}

运行结果:
在这里插入图片描述

时间戳原子引用
新增一个机制,修改版本号(类似时间戳)
线程修改一次版本号累加一次,这样当线程发现版本号不一致则修改失败。
下面我们代码演示如下:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {
   static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
   static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);//1是时间戳也就是版本号是1
   public static void main(String[] args) {
       System.out.println("********模拟ABA代码演示 开始**********");
       new Thread(()->{
           atomicReference.compareAndSet(100,101);//AB
           atomicReference.compareAndSet(101,100);//A
       },"t1").start();

       new Thread(()->{
           //暂停1秒钟t2线程,保证上面的t1线程完成了一次ABA操作
           try {
               TimeUnit.SECONDS.sleep(1);
               System.out.println(atomicReference.compareAndSet(100,2019)+"\t"+atomicReference.get());
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       },"t2").start();

       try {
           TimeUnit.SECONDS.sleep(2);//暂停2秒确保t1和t2线程执行完
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       System.out.println("********模拟ABA代码演示 结束**********");

       System.out.println("********解决ABA代码演示 开始**********");

       new Thread(()->{
           int stamp = atomicStampedReference.getStamp(); //初始版本号
           System.out.println(Thread.currentThread().getName()+"\t 第一次版本号"+stamp);
           try {
               TimeUnit.SECONDS.sleep(1);//暂停1秒
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
           System.out.println(Thread.currentThread().getName()+"\t 第二次版本号"+atomicStampedReference.getStamp());
           atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
           System.out.println(Thread.currentThread().getName()+"\t 第三次版本号"+atomicStampedReference.getStamp());
           },"t3").start();

       new Thread(()->{
           int stamp = atomicStampedReference.getStamp(); //初始版本号
           System.out.println(Thread.currentThread().getName()+"\t 第一次版本号"+stamp);
           try {
               TimeUnit.SECONDS.sleep(3);//暂停3秒,保证上面t3线程完成一次ABA操作
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
           System.out.println(Thread.currentThread().getName()+"\t 修改是否成功:"+result+"\t 最新版本号:"+atomicStampedReference.getStamp());
           System.out.println(Thread.currentThread().getName()+"\t 当前最新值:"+atomicStampedReference.getReference());
           },"t4").start();
   }
}

运行结果:
在这里插入图片描述
以上就是ABA 产生和解决方式。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值