三、JUC提升——java的CAS原理解析含ABA问题解决

Compare And Swap(比较并替换)

一、作用

我们的CAS是什么,用来干嘛的呢??
首先CAS就是一个值替换的操作,可以类比于java中的“=”号赋值操作,相当于修改变量的值!可是,既然“=”号可以实现,CAS又有什么意义呢?
正常单线程的情况下,确实没有必要使用CAS,可是,如果是高并发情况,如果不仅仅只是修改这个值,而是做一个“++“”的累加操作,用于计数(先跳过所谓的volatile,这里不引入这个话题,后续会写一下volatile)!
在“++”的计数过程中,我希望达到的效果是,无论同时有多少个线程来执行这个++的操作,我都能够保证,其结果是原子性的(就是有多少次++,最后结果就是多少)!

public class MyCasExample {
    private static int state=0;
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(100);
        for (int i = 0; i <100 ; i++) {
            new Thread(()->{
                for (int j = 0; j <100 ; j++) {
                    MyCasExample.state++;
                }
                // CountDownLatch类,可以理解成火箭发射,需要等待其他准备工作完成,完成一个任务(线程), 每次countDown就会减一,直到为0后,再统一释放所有线程!
                countDownLatch.countDown();
            }).start();
        }
        //这里为等待上面countDown收集线程,等上面减完,则会同时释放所有线程
        countDownLatch.await();
        System.out.println(MyCasExample.state);
    }
}

代码介绍:对静态属性state进行计数操作!为了模仿高并发场景,使用了工具类CountDownLatch(倒计时器),最后会呈现出100个线程,同时释放执行100次++的场景!不过并非每次结果都不对,需要多试试

通过这段代码,我们了解到了高并发场景下,++计数的操作,会导致什么问题!不过肯定有人说,那synchronized或者Lock加锁,不是也能解决哇?

我这里补充一句:CAS是无锁的方式,没有锁竞争(阻塞)带来的开销,也没有线程间频繁调度切换(与内核线程之间的切换执行)带来的开销,它比基于锁的方式有更优越的性能!

二、定义:

在JUC中,很多地方都用到的CAS,比如AQS中,更是处处存在,使用到了极致!同时原子包中atomic包的原子类也有很多基于CAS实现的方法!
有一句话解释这个:在高并发情况下,对一个变量用不加锁的方式来实现原子性操作!

1、什么是CAS机制

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值N,要修改的新值B

更新一个变量的时候,只有当变量的预期值A内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是通过自旋等待,同时被告知竞争失败,并可再次尝试!它是基于硬件平台的汇编指令。

2、CAS图解

在这里插入图片描述
CAS是非常非常重要的,如果CAS搞不清楚,那么AQS源码也必然看不懂!因为CAS常见于AQS(AbstractQueuedSynchronizer)和原子类中,同时CAS是Unsafe魔术类中的native修饰的方法!
在这里插入图片描述
如果有下载Openjdk源码,可以在,Hotspot中查看到具体的C代码!CAS底层使用JNI调用C代码实现的,可以在Unsafe.cpp里可以找到它的实现:
(以下部分可以不看!后方有链接,感兴趣可以去看看!)—————————————————————————————————————————————

static JNINativeMethod methods_15[] = {
    //省略一堆代码...
    {CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z",      FN_PTR(Unsafe_CompareAndSwapInt)},
    {CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z",      FN_PTR(Unsafe_CompareAndSwapLong)},
    //省略一堆代码...
};

我们可以看到compareAndSwapInt实现是在Unsafe_CompareAndSwapInt里面,再深入到Unsafe_CompareAndSwapInt:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

p是取出的对象,addr是p中offset处的地址,最后调用了Atomic::cmpxchg(x, addr, e), 其中参数x是即将更新的值,参数e是原内存的值。

(以上代码可以不用细看)—————————————————————————————————————————————
作为java开发,没必要研究更深入,我们用图形理解CAS的原理即可!

在这里插入图片描述我们来看看这个图:我们用简单易懂的话来讲:首先,我们这个CAS的操作,会优先从内存中拿到V=4,也就是我们想要操作的那个初始值!复制给N变量,N=4,然后执行我的业务代码,计算后,比如‘’++‘’2次后,得到6的结果,我们赋值给B,则B=6;此时,我们就需要把这个B=6,重新写回内存中,修改V=6!
此时!!!注意了,CAS会多一步判断,写这个代码的人认为,我这中间执行计算方法的时候,也就是那0.01ms的时间内,万一内存中的值被别的线程先改了呢?也就是我们内存中的初始值,已经发生了改变,变成了V=5;
那么,我们的V到底是更新呢??还是更新呢??还更新呢?
肯定,必然,是不能更新的!
那么如何判断这个值变没有?
那就是用最开始复制过来的旧的预期值N=4,和内存的值V=4进行比较,如果说,他们两个值一样,说明运气好,没人动它!
否则,如果不相等……说明运气不太好!那不相等怎么办?
当前CAS自旋,持续去执行,直到成功为止!
(额外内容:cmpxchg 不具有原子性,lock指令在执行后面指令的时候锁定一个北桥电信号,当执行cmpxchg 其他cpu不允许做修改,所以lock cmpxchg具有原子性。
所以cas还是会上锁,不过锁定北桥信号(不采用锁总线的方式)比锁定总线轻量,这就很好的解释了同时写入问题。)CAS详细底层C语言源代码,访问这个链接去研究吧:https://blog.csdn.net/AAA17864308253/article/details/105524056)。

3、ABA问题

这里是展示ABA的问题,也就是说,实际上我的原子类数据 1 ,已经改变过了!从1变为2 ,又改成1!可是,我们的cas代码还是正常执行,判断为原子类没有发生改变!产生了ABA的问题!对于数据来说,没什么,但是作为业务严谨性,明明发生了改变了,我们不能把这个1当成最初的1!也就是在开发中,不能说1一定等于1!(以下代码需要多执行几次,并非一次就能出现ABA问题,可能还会有CAS修改失败的问题!)

public class AtomicAbaProblemTest {
    static AtomicInteger atomicInteger = new AtomicInteger(1);
    public static void main(String[] args) {
        Thread mainThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = atomicInteger.get();
                System.out.println("操作---"+Thread.currentThread().getName()+"--修改前原子数值:"+a);
                try {
                    //把自己睡着,给别人机会
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                    //这里执行cas方法,看运气,如果原子类没变成1,则修改失败
                boolean isCasSuccess = atomicInteger.compareAndSet(a,3);
                if(isCasSuccess){
                    System.out.println("操作---"+Thread.currentThread().getName()+"--Cas修改后原子数值:"+atomicInteger.get());
                }else{
                    System.out.println("CAS修改失败");
                }
            }
        },"主线程");
        Thread interfThread = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInteger.incrementAndGet();// 1+1 = 2;
                System.out.println("操作---"+Thread.currentThread().getName()+"--increase增加后的值:"+atomicInteger.get());
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicInteger.decrementAndGet();// atomic-1 = 2-1;
                System.out.println("操作---"+Thread.currentThread().getName()+"--decrease减少后的值:"+atomicInteger.get());
            }
        },"干扰线程");
        mainThread.start();
        interfThread.start();
    }
}

那么我们如何判断出这个1不等于原来的1呢?
这里要用一个新的原子类!AtomicStampedReference,如果想知道详细的,延伸学习!

public class AtomicStampedRerenceTest {
    private static AtomicStampedReference<Integer> atomicStampedRef =
            new AtomicStampedReference<>(1, 0);
    public static void main(String[] args){
        Thread mainThread = new Thread(() -> {
            //获取当前标识也就是0,用来识别是否产生ABA的问题!
            int stamp = atomicStampedRef.getStamp();
            System.out.println("操作主线程:" + Thread.currentThread()+
                    "stamp="+stamp + ",初始值 a = " + atomicStampedRef.getReference());
            try {
                Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1);  //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
            System.out.println("操作主线程:" + Thread.currentThread() +
                    "stamp="+stamp + ",CAS操作结果: " + isCASSuccess+",值a="+atomicStampedRef.getReference());
        },"主操作线程");

        Thread interfThread = new Thread(() -> {
            int stamp = atomicStampedRef.getStamp();
            //第一个参数为初始值,第二个为新值,第三个为标识,第四个参数为新标识
            atomicStampedRef.compareAndSet(1,2,stamp,stamp+1);
            System.out.println("操作干扰线程:" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +"," +
                    "【increment】" +
                    " ,值a = "+ atomicStampedRef.getReference());
            //每操作一遍,stamp+1
            stamp = atomicStampedRef.getStamp();
            atomicStampedRef.compareAndSet(2,1,stamp,stamp+1);
            System.out.println("操作干扰线程:" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +"," +
                    "【decrement】" +
                    " ,值a = "+ atomicStampedRef.getReference());
        },"干扰线程");
        mainThread.start();
        interfThread.start();
    }

这段代码,加了标识后的原子类,可以通过额外的标识进行标记!只要标识不同,则不能进行CAS计算!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值