CAS是什么它的优缺点以及怎样解决ABA问题

    什么是CAS呢,它的全称是Compare And Swap,比较并交换,它是一条CPU并发原语,它的功能是判断内存中某个位置的值是否为预期值,如果是预期值则更改为新的值,这个过程是原子的,说到CAS,不得不说JUC包下的原子类,我们先来看一个demo。

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {

    public static void main(String[] args) {
        AtomicInteger ai = new AtomicInteger(5);

        System.err.println("ai被修改:" + ai.compareAndSet(5, 6) + "\t ai中的最新值" + ai.get());
        System.err.println("ai被修改:" + ai.compareAndSet(5, 7) + "\t ai中的最新值" + ai.get());
    }

}

运行结果

ai被修改:true     ai中的最新值6
ai被修改:false     ai中的最新值6

    我们模拟了两个线程,来修改原子类中的值,如果线程的期望值跟物理内存的真实值一样,我就修改我的更新值,如果期望值和真实值不一样就修改失败,从结果看出,ai第一次被修改成功,为6,当第二次想修改的时候,最新值和期望值不一样,所以修改失败。

    那CAS的底层原理是什么呢,我们就打开AtomicInteger类中的compareAndSet方法来看看

    public final boolean compareAndSet(int expectedValue, int newValue) {
        return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
    }

    这里的U呢,是private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();

    在顺着compareAndSetInt方法继续找

    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }

    从源码中看出CAS底层原理其实就是UnSafe类和自旋锁(do while),UnSafe是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据。UnSafe类存在于sun.misc包中,其内部方法操作可以像C语言的指针一样直接操作内存,因为Java中CAS操作的执行依赖于UnSafe类的方法。注意UnSafe类中的所有方法都是native修饰的,也就是说UnSafe类中的方法都直接调用操作系统底层资源执行响应方法。这里我们也看到,synchronized关键字,还可以保证原子性,说明原子类的强大,既然有优点,它肯定还是有缺点的,事情总是有双面性,它的缺点就是由于底层使用do...while循环时间长,对CPU的开销大,只能保证一个共享变量的原子操作,还会出现ABA现象。

    那什么是ABA,CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化,比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,然后线程1操作成功,尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

    那怎样解决ABA问题呢,有两个办法第一个呢

  • 使用原子引用类,它不局限于某个值,而是一个对象内存地址,我们看一个demo

import java.util.concurrent.atomic.AtomicReference;

public class ABADemo2 {

    public static void main(String[] args) {

        User z3 = new User("zhangsan", 18);
        User li4 = new User("lisi", 18);
        AtomicReference<User> ar = new AtomicReference<User>();
        ar.set(z3);

        System.err.println("ar被修改:" + ar.compareAndSet(z3, li4)+"\t User的最新值"+ar.get().toString());
        System.err.println("ar被修改:" + ar.compareAndSet(z3, li4)+"\t User的最新值"+ar.get().toString());
    }

}

class User {
    String name;
    int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User [name=" + name + ", age=" + age + "]";
    }

    public User() {
    }

    public User(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }

}

运行结果

ar被修改:true     User的最新值User [name=lisi, age=18]
ar被修改:false     User的最新值User [name=lisi, age=18]

    由于每个对象的内存地址不一样,所以能结果ABA问题。

 

  • 那第二个解决方法呢,就是修改版本号,类似于时间戳,也是JUC包下的AtomicStampedReference,我们继续看一个demo

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

public class ABADemo {

    static AtomicReference<Integer> ari = new AtomicReference<Integer>(100);
    static AtomicStampedReference<Integer> asri = new AtomicStampedReference<Integer>(100, 1);

    public static void main(String[] args) {

        new Thread(() -> {
            ari.compareAndSet(100, 101);
            ari.compareAndSet(101, 100);
        }, "thread 1").start();

        new Thread(() -> {

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

            System.err.println(ari.compareAndSet(100, 102) + "\t" + ari.get());
        }, "thread 2").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            // TODO 自動生成された catch ブロック
            e.printStackTrace();
        }
        System.err.println("***************以下是ABA问题的解决**************");

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

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            asri.compareAndSet(100, 101, asri.getStamp(), asri.getStamp() + 1);
            System.err.println(Thread.currentThread().getName() + "\t第二次版本号:" + asri.getStamp());
            asri.compareAndSet(101, 102, asri.getStamp(), asri.getStamp() + 1);
            System.err.println(Thread.currentThread().getName() + "\t第三次版本号:" + asri.getStamp());

        }, "thread 3").start();

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

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

            boolean modifyResult = asri.compareAndSet(101, 2020, asri.getStamp(), asri.getStamp() + 1);
            System.err.println(
                    Thread.currentThread().getName() + "\t修改结果:" + modifyResult + "\t当前版本号:" + asri.getStamp()
                            + "\t最新值:"
                            + asri.getReference());
        }, "thread 4").start();
    }

}

执行结果

true    102
***************以下是ABA问题的解决**************
thread 3    第一次版本号:1
thread 4    第一次版本号:1
thread 3    第二次版本号:2
thread 3    第三次版本号:3
thread 4    修改结果:false    当前版本号:3    最新值:102

    这个demo中类似于数据库中的乐观锁,每修改一次,将版本号加1,这样其他线程在修改前看版本号,期待的版本号和最新的版本号不一样的时候,修改失败,这样就解决了ABA问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值