CAS(compare and swap)

什么是CAS?

        CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

        乐观锁(多读场景):乐观锁采取了更加宽松的加锁机制。也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:

        悲观锁(多写场景):当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。-----悲观并发控制实际上是“先取锁再访问”的保守策略

package lin.cas;

import java.util.concurrent.atomic.AtomicInteger;

//CAS   compareAndSet:比较并交换
public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020); //2020是一个初始值
        //源码:  public final boolean compareAndSet(int expect, int update)
        //                                               期望         更新
        atomicInteger.compareAndSet(2020,2021); //如果我期望的值达到了,就更新,否则就不更新
        System.out.println(atomicInteger.get());
    }
}

结果:

CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么执行操作!如果不是,就一直循环!(因为底层是自旋锁),CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做

 

 

CAS缺点:

  1. 循环会耗时。CAS造成CPU利用率增加。之前说过了CAS里面是一个循环判断(因为底层是自旋锁)的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。
  2. 由于底层是一个CPU的操作,一次性只能保证一个共享变量的原子性
  3. 会存在ABA问题(狸猫换太子):两条线程同时去操作一个资源Q=1,线程A先执行,操作了资源Q,使用cas(1,2)改成了 Q=2,然后在cas(2,1)改成了Q=1,这时候线程B拿到的资源Q=1,就是已经被动过的资源
package lin.cas;

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

public class ABADemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020); //2020是一个初始值
        //线程A
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+" loaded");
            atomicInteger.compareAndSet(2020,2021);
            atomicInteger.compareAndSet(2021,2020);
        },"A").start();

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

        //线程B
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+" loaded");
            atomicInteger.compareAndSet(2020,6666);
        },"B").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println(atomicInteger.get());
    }
}

ABA问题(有两类)

第一类问题

我们考虑下面一种ABA的情况:

  1. 在多线程的环境中,线程a从共享的地址X中读取到了对象A。
  2. 在线程a准备对地址X进行更新之前,线程b将地址X中的值修改为了B。
  3. 接着线程b将地址X中的值又修改回了A。
  4. 最新线程a对地址X执行CAS,发现X中存储的还是对象A,对象匹配,CAS成功。

上面的例子中CAS成功了,但是实际上这个CAS并不是原子操作,如果我们想要依赖CAS来实现原子操作的话可能就会出现隐藏的bug。

第一类问题的关键就在2和3两步。这两步我们可以看到线程b直接替换了内存地址X中的内容。

在拥有自动GC环境的编程语言,比如说java中,2,3的情况是不可能出现的,因为在java中,只要两个对象的地址一致,就表示这两个对象是相等的。

2,3两步可能出现的情况就在像C++这种,不存在自动GC环境的编程语言中。因为可以自己控制对象的生命周期,如果我们从一个list中删除掉了一个对象,然后又重新分配了一个对象,并将其add back到list中去,那么根据 MRU memory allocation算法,这个新的对象很有可能和之前删除对象的内存地址是一样的。这样就会导致ABA的问题。

第二类问题

如果我们在拥有自动GC的编程语言中,那么是否仍然存在CAS问题呢?

考虑下面的情况,有一个链表里面的数据是A->B->C,我们希望执行一个CAS操作,将A替换成D,生成链表D->B->C。考虑下面的步骤:

  1. 线程a读取链表头部节点A。
  2. 线程b将链表中的B节点删掉,链表变成了A->C
  3. 线程a执行CAS操作,将A替换从D。

最后我们的到的链表是D->C,而不是D->B->C。

问题出在哪呢?CAS比较的节点A和最新的头部节点是不是同一个节点,它并没有关心节点A在步骤1和3之间是否内容发生变化。

AtomicStampedReference(原子引用)------有时间戳的原子引用

作用:可以解决第一类ABA问题,当线程A修改过了值以后,时间戳就会发生变化,后面其他线程再去使用这个值的时候,就知道是被线程A修改过后的值了。--------------------跟乐观锁的原理是一样的

 举例:使用有时间戳的原子引用,来实现CAS

package lin.cas;

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

public class ABADemo {
    public static void main(String[] args) {
//        AtomicInteger atomicInteger = new AtomicInteger(2020); //2020是一个初始值
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);//这个“1”就是时间戳(版本号)


        //线程A
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+" loaded");
//            atomicStampedReference.getStamp();//获得版本号
            System.out.println(Thread.currentThread().getName()+" StampA1->"+atomicStampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            atomicStampedReference.compareAndSet(100,101,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);

            System.out.println(Thread.currentThread().getName()+" StampA2->"+atomicStampedReference.getStamp());

            atomicStampedReference.compareAndSet(101,100,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);

            System.out.println(Thread.currentThread().getName()+" StampA3->"+atomicStampedReference.getStamp());
        },"A").start();

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

        //线程B
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+" loaded");
            System.out.println(Thread.currentThread().getName()+" StampB1->"+atomicStampedReference.getStamp());

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

            atomicStampedReference.compareAndSet(100,6666,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+" StampB2->"+atomicStampedReference.getStamp());

        },"B").start();

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

        System.out.println(atomicStampedReference.getReference());

    }
}

这里有一个大坑 AtomicStampedReference<Integer>,特别是Integer是,默认的大小范围在-127~+127之间,不然CAS时,不会成功!

        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(2020, 1);//这个“1”就是时间戳(版本号)

此时2020就已经超过了Integer的定义范围,之后的CAS操作返回值都为False!!!

注意:如果这其中的泛型是一个包装类时,就要注意对象的引用问题(正常的业务操作,泛型中都是一个个对象,比较的就是一个个对象)

        AtomicStampedReference<User> atomicStampedReference = new AtomicStampedReference<>(User, 1);//正常的业务操作时,泛型中基本都是一个个对象,比较的也是一个个对象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lin_XXiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值