首页 > 技术文章 > CAS&ABA的理解

mobiwusihuan288 2020-10-17 12:17 原文

学习地址:https://www.bilibili.com/video/BV18b411M7xz?p=12

CAS

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。(比较并替换)

它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的。

CAS并发原语提现在Java语言中就是sun.miscUnSaffe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我实现CAS汇编指令。这是一种完全依赖于硬件功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也即是说CAS是一条CPU原子指令,不会造成所谓的数据不一致的问题。

Demo

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2020) + "\t current data: " + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t current data: " + atomicInteger.get());
    }
}
true	 current data: 2020
false	 current data: 2020

// compareAndSet源码
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

this-当前对象
valueOffset-内存偏移量(内存地址)
为什么AtomicInteger能解决i++多线程下不安全的问题,靠的是底层的Unsafe类

Unsafe

  1. UnSafe是CAS的核心类 由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特额定的内存数据。UnSafe类在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作的助兴依赖于UNSafe类的方法。

    • 注意UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务
  2. 变量ValueOffset,表示该变量值在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的。

  3. 变量value用volatile修饰,保证了多线程之间的内存可见性。

img


image-20201017111255326


unsafe.getAndAddInt

img


var1:AtomicInteger对象本身;
var2:该对象值的引用地址;
var4:需要变动的数值;
var5:是用过var1 var2找出内存中绅士的值。

用该对象当前的值与var5比较:
如果相同,更新var5的值并且返回true
如果不同,继续取值然后比较,直到更新完成


假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上):

  1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。

  2. 线程A通过getIntVolatile(var1,var2) 拿到value值3,这时线程A被挂起。

  3. 线程B也通过getIntVolatile(var1,var2) 拿到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存中的值也是3,成功修改内存的值为4,线程B打完收工,一切OK。

  4. 这是线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和内存中的值数字4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了。

  5. 线程A重新获取value值,因为变量value是volatile修饰,所以其他线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功。

底层汇编

image-20201017115307750

小结

CAS ( CompareAndSwap )

比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止.

CAS应用

CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

缺点

  1. 循环时间长开销很大
    • getAndAddInt()方法有个 do while循环
    • 如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销
  2. 只能保证一个共享变量的原子性
    • 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

ABA

CAS会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。

尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

Demo

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 z3 = new User("z3",22);
        User li4 = new User("li4",25);

        AtomicReference<User> atomicReference = new AtomicReference<>();

        atomicReference.set(z3);
        System.out.println(atomicReference.compareAndSet(z3,li4)+"\t"+atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(z3,li4)+"\t"+atomicReference.get().toString());

    }
}
true	User(userName=li4, age=25)
false	User(userName=li4, age=25)

时间戳原子引用

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

/**
 * ABA问题的解决  AtomicStampedReference
 */
public class ABADemo {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);


    public static void main(String[] args) {
        System.out.println("======以下是ABA问题的产生=====");
        new Thread(() -> {
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();

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

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("======以下是ABA问题的解决=====");
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);

            //暂停1秒钟t3线程
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第2次版本号:" + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第3次版本号:" + atomicStampedReference.getStamp());
        }, "t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);

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

            System.out.println(Thread.currentThread().getName() + "\t当前实际最新的值: " + atomicStampedReference.getReference());

        }, "t4").start();
    }
}
======以下是ABA问题的产生=====
true	2020
======以下是ABA问题的解决=====
t3	第1次版本号:1
t4	第1次版本号:1
t3	第2次版本号:2
t3	第3次版本号:3
t4	修改成功否: false	当前最新实际版本号:3
t4	当前实际最新的值: 100

推荐阅读