CAS简单理解

CAS

在介绍CAS之前,我们先看一段代码

@Slf4j
public class CASTest {
    public static volatile int race = 0;
    public static final int THREAD_COUNT = 20;

    public static void increase(){
        race++;
    }

    //20个线程
    private static final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT,new ThreadNameFactory());

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < THREAD_COUNT; i++) {
            log.debug("第{}个线程",i);
            //20 * 10,000 = 200,000  输出结果不为200000
            executorService.execute(new ThreadIncrease());
        }

        Thread.sleep(10000);

        System.out.println(race);

    }


    //线程工厂,用来定义线程名称
    static class ThreadNameFactory implements ThreadFactory{
        private static final AtomicInteger atomicInteger = new AtomicInteger(0);

        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r,atomicInteger.getAndIncrement()+"");
        }
    }

    //线程执行方法体
    static class ThreadIncrease implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                increase();
            }
            log.debug("第{}个线程运行结束",Thread.currentThread().getName());
        }
    }
}

这段代码,无论怎么运行,结果多不会是20000(可能某一次误打误撞会是),这是为什么?

我们知道,volatile关键字是一个轻量级的同步框架,在运行时时,使用volatile修饰的变量的改变对其他线程是可见的。

但是volatile只保证了可见性,没有保证原子性,所以答案就不会是200000。

想让这段代码运行成功很简单,在increase()方法上加sychnorized,但是这样性能会大大降低。

那该怎么做呢?使用原子操作类的变量就可了,以下是更改后的代码:

	public static AtomicInteger race = new AtomicInteger(0);
    
    public static void increase(){
        race.getAndIncrement();
    }

再次运行,这次结果就是200000了。

那么原子操作类是怎么保证运行的呢?我们先看getAndIncrement()的源码:

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

我们接着往下看,看getAndAddInt这个方法:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
}

在这段代码中,有用的其实就是这个方法compareAndSwapInt(),他就是我们要介绍的CAS了

compareAndSwapInt()方法解析:

拿到内存位置的新值var5,使用CAS尝试修改为var5+var4,如果失败,获取新值var5,然后继续,直到成功

介绍

CAS是CompareAndSwap的缩写,比较并替换。CAS底层有三个操作数:

  • 内存地址V
  • 旧值A
  • 即将要更改的值B

CAS在运行时,当且仅当内存地址的V的值与预期值A相等,才会将内存地址对应的V值改为B,具体流程如下:

  • 线程1的内存地址V的值为10,此时线程1想要把V的值修改为11,所以此时A = 10,B=11
  • 但是在线程1提交之前,线程2抢先一步提交了,把V的值设置为11了,此时线程1将提交失败(因为A=10 != V=11)
  • 而后线程1重新获取内存地址V值,并重新计算要修改的值,假设将修改为12,那么此时V=11,A=11,B=12
  • 此时进行对别,V=A==11,所以提交成功,V被修改为12
  • 到此CAS操作完成

在上述过程中,程1重新获取内存地址V值,并重新计算要修改的值的过程称为自旋

从思想上来说,CAS属于乐观锁,认为程序并发情况没那么严重,所以会尝试不断更新,而synchronized被称为悲观锁

缺点

CAS虽然高效的解决了原子操作问题,但是CAS仍然存在三个大问题:

  • 循环时间长,开销大

    • 如果不成功,会一直循环,给CPU带来很大的开销
  • 只能保证一个共享变量的原子操作

    • 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
  • ABA问题:

    • CAS的过程如下:

      1. 从地址V中读取A值
      2. 根据A计算B
      3. 通过CAS将V修改为B

      但是在第一步中读取的是A,并且在第三步修改成功,但是第二步呢?可不可能被其他线程修改过?答案是肯定的,如果这期间被第二个线程修改过,他的值被改为了B,后来又经过又被改为A,那么CAS就会认为他从来没被改变过(其实目的还是为了改成A),所以继续又改成了B(显然不是我们想要的),这个问题就被称为ABA问题

      Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

你可能感兴趣的:(java,CAS)