JUC-CAS介绍和原子引用(十五)

一、什么是CAS

CAS 是 Compare And Swap的简称,从字面上理解就是比较并更新,简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。

在 Java的原子类中,有对cas的封装进行应用:此处用 AtomicInteger 举例

public class CAS_Demo {

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        //CAS的封装体现:compareAndSet 比较并赋值
        //比较当前值是否为2020? set 2021,return true : do nothing,return false
        System.out.println(atomicInteger.compareAndSet(2020, 2021));//true
        System.out.println(atomicInteger.get());//此时为2021

        //获取值并自增
        atomicInteger.getAndIncrement();//自增后为 2022
        System.out.println(atomicInteger.compareAndSet(2021, 2033));//false
        System.out.println(atomicInteger.get());// 2022
    }
}

点击查看AtomicInteger的源码:
直接通过Unsafe类调用底层 CAS方法

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

//getAndIncrement  
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

在这里插入图片描述

在这里插入图片描述
getAndAddInt方法详解:
在这里插入图片描述
通过一定的轮询,也就是后面介绍的自旋锁
在这里插入图片描述
CAS锁其又称作Java的乐观锁,但是我们可以看到其实质上是没有上锁的,只是赋值的过程前多了一个比较的方法,因此可能引起一定的ABA问题:
[狸猫换太子] 如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。

举个栗子:
在你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。

public class CAS_Demo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        // 我们想把数字改成6666
        // ============== 捣乱的线程 ==================
        atomicInteger.compareAndSet(2020, 2021);
        System.out.println("such a fool...");
        atomicInteger.compareAndSet(2021, 2020);

        // ============== 期望的线程 ==================
        System.out.println(atomicInteger.compareAndSet(2020, 6666));
        System.out.println(atomicInteger.get());
    }
}

解决上述问题的一个策略是:
每一次倒水假设有一个自动记录仪记录下,也就是给操作赋值上一个标致,这样主人回来就可以分辨在他离开后是否发生过重新倒满的情况。

这也是解决ABA问题目前采用的策略,这就需要我们的原子引用

二、原子引用

官方说明:

一个AtomicStampedReference维护对象引用以及整数“印记”,可以原子更新。
实现注意事项:此实现通过创建表示“boxed”[引用,整数]对的内部对象来维护加盖引用。

举个例子:

这里用到了双参构造方法:(一个初始值,一个时间戳)

public class CAS_ABA_Demo {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> intAtomicStampedReference = new AtomicStampedReference<>(1, 1);

        //A线程
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "操作前stamp:A1==>"+ intAtomicStampedReference.getStamp() + " 值:"+intAtomicStampedReference.getReference());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ignored) {}

            intAtomicStampedReference.compareAndSet(1,2,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "操作后stamp:A2==>"+ intAtomicStampedReference.getStamp() + " 值:"+intAtomicStampedReference.getReference());

            intAtomicStampedReference.compareAndSet(2,1,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "回滚操作stamp:A3==>"+ intAtomicStampedReference.getStamp() + " 值:"+intAtomicStampedReference.getReference());
        },"线程A ").start();

        //B线程
        new Thread(()->{
            int stamp = intAtomicStampedReference.getStamp(); // 获得版本号
            System.out.println(Thread.currentThread().getName() + "操作前stamp:B=>"+ stamp + " 值:"+intAtomicStampedReference.getReference());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException ignored) {}
            System.out.println(Thread.currentThread().getName() + "操作是否成功:"+intAtomicStampedReference.compareAndSet(1, 6,
                    stamp, stamp + 1));
            System.out.println(Thread.currentThread().getName() + "操作后stamp:b2=>"+intAtomicStampedReference.getStamp() + " 值:"+intAtomicStampedReference.getReference());
        },"线程B ").start();
    }
}

执行结果:

线程A 操作前stamp:A1==>1 值:1
线程B 操作前stamp:B=>1 值:1
线程A 操作后stamp:A2==>2 值:2
线程A 回滚操作stamp:A3==>3 值:1
线程B 操作是否成功:false
线程B 操作后stamp:b2=>3 值:1

线程B因为版本stamp不一致的问题的问题,导致无法修改成功。
此时就不会由于stamp,也可理解为数据库中乐观锁的版本号,时的赋值操作失败,修改不成功。

三、Integer包装类的小坑

在原子引用的示例中,是由1变为2,再2变为1。若将测试示例改为由2021变为2022,再2022变为2021。会发现示例测试无法成功,版本号不会改变。

public class CAS_ABA_Demo {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> intAtomicStampedReference = new AtomicStampedReference<>(2021, 1);

        //A线程
        new Thread(()->{
            System.out.println("操作前stamp:A1==>"+ intAtomicStampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ignored) {}

            intAtomicStampedReference.compareAndSet(2021,2022,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
            System.out.println("操作后stamp:A2==>"+ intAtomicStampedReference.getStamp());

            intAtomicStampedReference.compareAndSet(2022,2021,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
            System.out.println("回滚操作stamp:A3==>"+ intAtomicStampedReference.getStamp());

            System.out.println("假装是 x="+intAtomicStampedReference.getReference());
        },"A").start();

        //B线程
        new Thread(()->{
            int stamp = intAtomicStampedReference.getStamp(); // 获得版本号
            System.out.println("操作前stamp:B=>"+ stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException ignored) {}
            System.out.println("操作是否成功:"+intAtomicStampedReference.compareAndSet(1, 6,
                    stamp, stamp + 1));
            System.out.println("操作后stamp:b2=>"+intAtomicStampedReference.getStamp());
            System.out.println("此时的值:"+intAtomicStampedReference.getReference());

        },"B").start();
    }
}

结果:

操作前stamp:A1==>1
操作前stamp:B=>1
操作后stamp:A2==>1
回滚操作stamp:A3==>1
假装是 x=2021
操作是否成功:false
操作后stamp:b2=>1
此时的值:2021

先看看intAtomicStampedReference.compareAndSet源码:

    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

current.reference取得的是初始化2021,不清楚的可以看下AtomicStampedReference构造方法源码。
断点打在expectedReference == current.reference这行代码时,可以看到明明两个值都为2021,却比较得false。
在这里插入图片描述
原因:
Integer 使用了对象缓存机制,默认范围是 -128 ~ 127 ,推荐使用静态工厂方法 valueOf 获取对象实
例,而不是 new,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间;

在这里插入图片描述
可以自己测试一下:
在这里插入图片描述

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值