CAS原理

348 阅读4分钟

CAS原理

CAS即Compare And Swap的缩写,翻译成中文就是比较并交换,其作用是让CPU比较内存中某个值是否和预期的值相同,如果相同则将这个值更新为新值,不相同则不做更新,也就是CAS是原子性的操作(读和写两者同时具有原子性),其实现方式是通过借助C/C++调用CPU指令完成的,所以效率很高。 CAS的原理很简单,这里使用一段Java代码来描述。

public boolean compareAndSwap(int value, int expect, int update) {
//        如果内存中的值value和期望值expect一样 则将值更新为新值update
    if (value == expect) {
        value = update;
        return true;
    } else {
        return false;
    }
}

CAS缺点

1.ABA问题

在多线程场景下CAS会出现ABA问题,关于ABA问题这里简单科普下,例如有2个线程同时对同一个值(初始值为A)进行CAS操作,这两个个线程如下

  • 线程1,期望值为A,欲更新的值为B
  • 线程2,期望值为A,欲更新的值为B

线程1抢先获得CPU时间片,而线程2因为其他原因阻塞了,线程1取值与期望的A值比较,发现相等然后将值更新为B,然后这个时候出现了线程3,期望值为B,欲更新的值为A,线程3取值与期望的值B比较,发现相等则将值更新为A,此时线程2从阻塞中恢复,并且获得了CPU时间片,这时候线程2取值与期望的值A比较,发现相等则将值更新为B,虽然线程2也完成了操作,但是线程2并不知道值已经经过了A->B->A的变化过程

解决:

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值

例子:

一家蛋糕店,决定为vip卡中余额小于20的用户一次性赠送20元,刺激消费,但是一个客户只能被赠送一次。 使用AtomicReference实现如下:

public class AtomicReferenceDemo {
    static AtomicReference<Integer> money=new AtomicReference<Integer>();
    public static void main(String[] args) {
        money.set(19);
        //模拟多个线程同时更新后台数据库,为用户充值
        for(int i = 0 ; i < 3 ; i++) {              
            new Thread() {  
                public void run() {  
                    while(true){
                        while(true){
                            Integer m=money.get();
                            if(m<20){
                                if(money.compareAndSet(m, m+20)){
                                    System.out.println("余额小于20元,充值成功,余额:"+money.get()+"元");
                                    break;
                                }
                            }else{
                                //System.out.println("余额大于20元,无需充值");
                                break ;
                            }
                        }
                    }
                }  
            }.start();
        }
        
        //用户消费线程,模拟消费行为
        new Thread() {  
            public void run() {  
                for(int i=0;i<100;i++){
                    while(true){
                        Integer m=money.get();
                        if(m>10){
                            System.out.println("大于10元");
                            if(money.compareAndSet(m, m-10)){
                                System.out.println("成功消费10元,余额:"+money.get());
                                break;
                            }
                        }else{
                            System.out.println("没有足够的金额");
                            break;
                        }
                    }
                    try {Thread.sleep(100);} catch (InterruptedException e) {}
                }
            }  
        }.start();  
    }

}

但是会存在一种问题,如果在赠与金额到达的同时,客户进行一次消费,使得总金额又小于20,并且正好累计消费20,使得消费、赠与后的金额等于消费前、赠与前的金额,那么,就会存在多次赠与的问题(ABA问题)。

主要原因就是因为对象在修改过程中丢失了状态信息,所以需要使用AtomicStampedReference来对对象进行更新,它的内部不仅维护了对象值,还维护了一个时间戳。

public class AtomicStampedReferenceDemo {
    static AtomicStampedReference<Integer> money=new AtomicStampedReference<Integer>(19,0);
    public static void main(String[] args) {
        //模拟多个线程同时更新后台数据库,为用户充值
        for(int i = 0 ; i < 3 ; i++) {
            final int timestamp=money.getStamp();
            new Thread() {  
                public void run() {  
                    while(true){
                        while(true){
                            Integer m=money.getReference();
                            if(m<20){
                                if(money.compareAndSet(m, m+20,timestamp,timestamp+1)){
                                    System.out.println("余额小于20元,充值成功,余额:"+money.getReference()+"元");
                                    break;
                                }
                            }else{
                                //System.out.println("余额大于20元,无需充值");
                                break ;
                            }
                        }
                    }
                }  
            }.start();
        }
        
        //用户消费线程,模拟消费行为
        new Thread() {  
            public void run() {  
                for(int i=0;i<100;i++){
                    while(true){
                        int timestamp=money.getStamp();
                        Integer m=money.getReference();
                        if(m>10){
                            System.out.println("大于10元");
                            if(money.compareAndSet(m, m-10,timestamp,timestamp+1)){
                                System.out.println("成功消费10元,余额:"+money.getReference());
                                break;
                            }
                        }else{
                            System.out.println("没有足够的金额");
                            break;
                        }
                    }
                    try {Thread.sleep(100);} catch (InterruptedException e) {}
                }
            }  
        }.start();  
    }
}

2.循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,

  1. 第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
  2. 第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3.只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

参考:www.iteye.com/blog/zl1987…