Java并发JUC(十一)

80 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情
本文主要介绍CAS(比较并交换),以及CAS存在的ABA问题(狸猫换太子)及原子引用来解决ABA问题。

20. CAS、ABA问题及原子引用

什么是CAS

CAS:compareAndSet比较并交换。与期望值进行比较,如果是期望的值,就更新,否则不更新。CAS是CPU的并发原语。

如下述代码,用原子类,设置初始2020,期望2020,如果达到期望2020,就更新成2021。

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        //参数1期望,参数2更新
//        public final boolean compareAndSet(int expect, int update)
//        如果期望的值达到了就更新,否则就不更新,CAS是CPU的并发原语
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
​
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
​
    }

当然,CAS也有一些缺点:

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

什么是ABA问题呢

ABA问题类似于狸猫换太子。比如说有两个线程,初始共享变量是1,其中A线程CAS1期望值是1,更新值是2,CAS2期望值是2,更新值是1;B线程期望值是1,更新值是2,且B做了个sleep,慢一点。针对这种情况,线程A将共享变量从1更新到2,又从2更新到1,但是B全然不知的。

代码举例:

public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(2020);
    // 期望、更新
    // public final boolean compareAndSet(int expect, int update)
    // 如果我期望的值达到了,那么就更新,否则,就不更新, CAS 是CPU的并发原语!
    // ============== 捣乱的线程 ==================
    System.out.println(atomicInteger.compareAndSet(2020, 2021));
    System.out.println(atomicInteger.get());
    System.out.println(atomicInteger.compareAndSet(2021, 2020));
    System.out.println(atomicInteger.get());
    // ============== 期望的线程 ==================
    System.out.println(atomicInteger.compareAndSet(2020, 6666));
    System.out.println(atomicInteger.get());
}

针对这种问题呢,就要用到原子引用了,原子引用对应的思想是乐观锁的思想。

什么是原子引用

简单说:原子引用就是一种带版本号的原子操作。

下面的代码做的事情就是:原子操作加了版本号,更新的时候会更新版本号。线程A先进行了带版本号的CAS,更新原始值为2,更新成功,然后又将2更新成1,此时版本号变成了3(初始版本号1,更新了两次+1)。B线程去做CAS,发现数值正确但版本号不正确,所以更新失败。

这里要注意的几个点:

  • integer有个坑,integer使用了对象缓存机制,默认范围是-128~127(如果integer不在这个范围,下面代码的CAS会出现线程B更新成功的问题),推荐使用静态工厂方法valueof获取对象实例,而不是new,因为valueof使用缓存,而new一定会创建新的对象分配新的内存空间。这是阿里巴巴java规范中提到的。

image.png

  • 所有包装类的比较,全部使用equals方法比较
public static void main(String[] args) {
    AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
    new Thread(()->{
        int stamp = atomicStampedReference.getStamp();//获得版本号
        System.out.println("a1=>" + stamp);
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(),
                atomicStampedReference.getStamp() + 1));
        System.out.println("a2=>" + atomicStampedReference.getStamp());
        System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(),
                atomicStampedReference.getStamp() + 1));
        System.out.println("a2=>" + atomicStampedReference.getStamp());
    },"a").start();
​
    new Thread(()->{
        int stamp = atomicStampedReference.getStamp();//获得版本号
        System.out.println("b1=>" + stamp);
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(atomicStampedReference.compareAndSet(1, 6, stamp, stamp + 1));
        System.out.println("b2=>" + atomicStampedReference.getStamp());
    },"b").start();
​
}