JUC并发编程(十一)--深入理解CAS,以及ABA问题

JJUC并发编程(十一)--深入理解CAS,以及ABA问题

一、CAS

CAS的操作过程:首先读取主内存位置M的原值到自己的工作内存,记为E,然后计算新值V,然后将主内存位置M的值与E比较(compare),如果相等,则在此过程中说明没有其它线程来修改过这个值,所以把内存位置M的值更新成V(swap)

直接看代码演示:

package com.zhan.juc.cas;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * CAS  compareAndSet:比较并交换
 * @Author Zhanzhan
 * @Date 2021/2/5 20:35
 */
public class CASDemo {
    public static void main(String[] args){
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        /**
         * 期望、更新
         * 如果我的期望的值达到了,那么就更新,否则,就不更新
         * CAS是 CPU 的并发原语
         */
        System.out.println(atomicInteger.compareAndSet(2020, 2021)); // 如果是期望的值2020,就更新为2021
        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(2020, 2021)); // 如果是期望的值2020,就更新为2021
        System.out.println(atomicInteger.get());
    }
}

那么现在,我们深入看compareAndSet()的源码:
在这里插入图片描述
发现,里面调用了U.compareAndSetInt(),那么U是什么?
在这里插入图片描述

二、Unsafe类

上面提到的Unsafe类是干嘛的呢?我们知道,Java无法直接操作内存,可以通过调用C++来操作内存,就是常见的 native方法,Unsafe类就是Java的一个可以操作内存的类。
以上面原子类中的 ++ 这个操作为例:
在这里插入图片描述
我们看这个方法的源码:
在这里插入图片描述
上图中的VALUE是什么呢?我们在看这个方法的源码之前,先搞懂这个参数:
在这里插入图片描述
原来指的是在内存中分配的位置。
那知道了这个参数后,我们继续进 getAndAddInt() 里看源码:
在这里插入图片描述
我们看到,while里面,调用了一个方法weakCompareAndSetInt(o, offset, v, v + delta),我们看这个方法:
在这里插入图片描述
这时,我们结合起来看,这个原子类的递增 +1 的操作,其实底层用的就是一个do while的自旋锁的机制,然后里面是用的就是CAS机制,比较和更新,比较期望值,然后更新值。是基于内存的操作。
由此,我们可以看出,CAS其实是一种不同于lock和synchronized的机制,里面没有锁,属于乐观锁的无锁策略,一旦检测到冲突,就在do while里循环,重复当前操作,直到没有冲突为止。

缺点:

  • 循环会耗时;
  • 一次性只能保证一个共享变量的原子性;
  • 会产生ABA问题。

三、ABA问题

假设这里有两个线程线程1和线程2,

线程1工作时间需要10秒,
线程2工作需要2秒,

主内存值为A,

第一轮线程1和线程2都把A拿到自己的工作内存,

2秒中后线程2工作完成把A改成了B再写回去,又过了2秒,线程2把B改成了A再写回去,然后就线程2进入休眠状态,

这时候线程1工作完成,看到期望为A真实值也是A认为没有人动过,然后线程1进行CAS操作。。

这就是狸猫换太子,即事先动过了原始值。
用代码来演示下:

package com.zhan.juc.cas;

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

/**
 * <p>CAS机制引起的ABA问题。</p>
 * <p>这里我们用两个线程,然后线程1先执行,将2020改为2021,然后又改回2020</p>
 * <p>然后线程2执行完计算,将计算后的值与主存中的值对比,发现是期望值,就执行了CAS操作,更新了期望值</p>
 * <p>我们原本希望的是,主存的值被改动过,其他线程需要得知这个情况</p>
 * @Author Zhanzhan
 * @Date 2021/2/5 21:48
 */
public class ABADemo {
    static AtomicReference<String> atomicReference = new AtomicReference<>("2020");

    public static void main(String[] args) throws InterruptedException {

        System.out.println("======= ABA 问题 ========");

        new Thread(() -> {
            System.out.println("将原始值改为 2021");
            atomicReference.compareAndSet("2020", "2021");
            System.out.println(atomicReference.get());
            System.out.println("哎嘿,我又改回来了-_-");
            atomicReference.compareAndSet("2021", "2020");
            System.out.println(atomicReference.get());
        }, "线程1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2L); // 线程休眠2秒,保证线程1先执行完,以便能模拟出来问题
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet("2020", "1000"));
            System.out.println(atomicReference.get());
        }, "线程2").start();

        TimeUnit.SECONDS.sleep(5L);
        System.out.println("讲述下为什么不用Integer作为泛型类");

        /**
         * int再包装为Integer时,如果int值在-128到127之间,会直接从缓存中获取;否则会直接new一个新的Integer对象。(缓存最大值一般就是127,有可能会通过虚拟机配置改为其他值)
         * 所以如果我们为两个Integer对象赋的值不在-128和127之间,则就会是两个不同的对象,用==比较,自然是返回false,
         * 所以上述的AtomicReference类中的泛型,如果示例中的值超过127,则就是两个对象,比较时肯定不是期望值,所以用String来作为泛型
         */
        Integer a = 2020;
        Integer b = 2020;
        System.out.println(a == b);
    }
}

四、原子引用

为了解决ABA问题,这里我们可以参考mysql的乐观锁解决方案,引入版本号。
上代码:

package com.zhan.juc.cas;

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

/**
 * <p>CAS机制引起的ABA问题。</p>
 * <p>这里我们用两个线程,然后线程1先执行,将2020改为2021,然后又改回2020</p>
 * <p>然后线程2执行完计算,将计算后的值与主存中的值对比,发现是期望值,就执行了CAS操作,更新了期望值</p>
 * <p>我们原本希望的是,主存的值被改动过,其他线程需要得知这个情况</p>
 * @Author Zhanzhan
 * @Date 2021/2/5 21:48
 */
public class ABADemo {
    static AtomicReference<String> atomicReference = new AtomicReference<>("2020");

    static AtomicStampedReference<String> stampedReference = new AtomicStampedReference<>("2020", 1);// 这里是带版本号的原子类

    public static void main(String[] args) throws InterruptedException {

        System.out.println("======= ABA 问题 ========");

        new Thread(() -> {
            System.out.println("将原始值改为 2021");
            atomicReference.compareAndSet("2020", "2021");
            System.out.println(atomicReference.get());
            System.out.println("哎嘿,我又改回来了-_-");
            atomicReference.compareAndSet("2021", "2020");
            System.out.println(atomicReference.get());
        }, "线程1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1L); // 线程休眠2秒,保证线程1先执行完,以便能模拟出来问题
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet("2020", "1000"));
            System.out.println(atomicReference.get());
        }, "线程2").start();

        TimeUnit.SECONDS.sleep(3L);

        System.out.println("====== ABA 问题解决 ======");

        new Thread(() -> {
            int stamp = stampedReference.getStamp(); // 获取版本号
            System.out.println("线程3 第一次操作时的版本号:" + stamp);

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

            System.out.println("将原始值改为 2021");
            // 每操作一次,版本号+1
            stampedReference.compareAndSet("2020", "2021", stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(stampedReference.getReference());
            
            System.out.println("哎嘿,我又改回来了-_-");
            System.out.println("线程3 第二次操作时的版本号:" + stampedReference.getStamp());
            stampedReference.compareAndSet("2021", "2020", stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(stampedReference.getReference());
        }, "线程3").start();

        new Thread(() -> {
            int stamp = stampedReference.getStamp(); // 获取版本号
            System.out.println("线程4 第一次操作时的版本号:" + stamp);

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

            System.out.println(stampedReference.compareAndSet("2020", "2021", stamp, stamp + 1));
        }, "线程4").start();


        /**
         * int再包装为Integer时,如果int值在-128到127之间,会直接从缓存中获取;否则会直接new一个新的Integer对象。(缓存最大值一般就是127,有可能会通过虚拟机配置改为其他值)
         * 所以如果我们为两个Integer对象赋的值不在-128和127之间,则就会是两个不同的对象,用==比较,自然是返回false,
         * 所以上述的AtomicReference类中的泛型,如果示例中的值超过127,则就是两个对象,比较时肯定不是期望值,所以用String来作为泛型
         */
        TimeUnit.SECONDS.sleep(10L);
        System.out.println("讲述下为什么不用Integer作为泛型类");
        Integer a = 2020;
        Integer b = 2020;
        System.out.println(a == b);
    }
}


看结果:
在这里插入图片描述
符合预期。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值