Android 并发编程CAS

1.CAS(比较并交换)

CAS指compare and swap,用于在多线程环境下实现同步功能,是一种无锁优化。它的作用是让CPU比较内存中某个值是否和预期的值相同,如果相同则将这个值更新为新值,不相同则不做更新。CAS是原子性的操作,其实现方式是通过借助C/C++调用CPU指令完成的,同时其避免了上锁、阻塞等一系列操作,效率非常高

当多个线程尝试使用CAS同时更新同一个变量时,只有一个线程可以更新变量的值,其他线程都会失败,失败的线程并不会挂起,而是告知这次竞争中失败了,并可以再次尝试。

①原子性

假定有两个操作A和B(A和B可能都很复杂),如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。

要实现原子操作可以使用锁,锁机制可以满足基本的需求。synchronized就是基于阻塞的锁机制,当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁。在这个过程中阻塞会造成线程上下文的切换,线程间切换实际是需要做很多事情的,比如要保存当前线程执行的指令以及它的程序计数器、内部的一些数据等。

这时候可以使用CAS。

synchronized是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。

CAS是一种乐观锁,它每次不加锁,假设没有冲突就去完成某项操作,如果因为冲突失败就重试,直到成功为止。

CAS是一种非阻塞算法,一个线程的失败或者挂起不应该影响其他线程的失败或挂起。

②CAS

使用CAS指令实现原子操作更加灵活。每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作时如果这个内存地址上存放的值等于期望的值A,就将地址上的值赋为新值B,否则不做任何操作。

d57c6bd7fb954d63bee40947ed8c3707.webp

循环CAS就是在一个循环里不断的做cas操作,直到成功为止。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_10,color_FFFFFF,t_70,g_se,x_16

 

2.CAS的缺点

①循环时间长,CPU开销较大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。即自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

②只能保证一个共享变量的原子操作。

当对一个共享变量执行操作时,可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。即CAS机制能保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性更新,就不得不使用Synchronized了。

解决办法:可以用锁;把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

注:其实对于线程安全,CAS和Synchronized这两种机制没有绝对的好与坏,关键得看场景,在并发量高的情况下,反而使用使用Synchronize更合适。

 

3.ABA问题

CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说线程1从内存位置V中取出A,这时候线程2也从内存中取出A,并且线程2进行了一些操作变成了B,然后又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,然后操作成功。尽管线程4的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

head.compareAndSet(A,B);

在T1执行上面这条指令之前,线程T2介入,将全部元素出栈,再将A、B入栈,此时堆栈结构发生变化;此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

这就是由于ABA问题带来的隐患,为了解决这个问题,可以使用版本戳,每修改一次,则版本号加1。在比较的时候不仅要比较当前变量的值,还需要比较当前变量的版本号。

各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中AtomicStampedReference也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。

AtomicStampedReference内部维护了对象值和版本号,在创建AtomicStampedReference对象时需要传入初始值和初始版本号。它会检查当前引用是否等于预期值引用,其次检查当前标志是否等于预期标志,如果都相等就会以原子的方式将引用和标志都设置为新值。

private static AtomicStampedReference< String> asr = new AtomicStampedReference< >("A", 1);

//线程1修改对象的值为A->B->A

new Thread(() -> {       

    Log.e(TAG,  "线程1拿到的当前时间戳版本号为:" + asr.getStamp());

    //休眠1秒,让线程2也拿到同样的初始版本号

    TimeUnit.SECONDS.sleep(1);

    //通过CAS自旋算法锁修改值

    atomicStampedReference.compareAndSet( "A", "B", asr.getStamp(), asr.getStamp() + 1);

    asr.compareAndSet("B", "A", asr.getStamp(), asr.getStamp() + 1);

    Log.e(TAG, "线程1完成修改:A ->B -> A");

}, "线程1").start();

//线程2去读取内存值并设置新值

new Thread(() -> {

    int stamp = asr.getSt amp();

    Log.e(TAG, "线程2拿到的当前时间戳版本号为:" + stamp);

    //线程休眠2秒,为了让线程A完成ABA操作

    TimeUnit.SECONDS.sleep(2);

    //判断是否修改成功

    boolean isSuccess = asr.compareAndSet("A", "C", stamp, asr.getStamp() + 1);

    Log.e(TAG, "最新版本号:" + asr.getStamp() + ",线程2是否修改成功:" + isSuccess + ",当前值是:" + asr.getReference());

}, "线程2").start();

程序运行结果:

线程1拿到的当前时间戳版本号为:1

线程2拿到的当前时间戳版本号为:1

线程1完成修改:A ->B -> A

最新版本号:3,线程2是否修改成功:false,当前值是A

根据log可以看出:线程1完成CAS操作后,最新版本号已经变成3,线程2执行修改操作时,由于与之前拿到的版本号1不相等,所以操作失败。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值