cas详解

641 阅读2分钟

「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。

cas和aqs是java并发编程的基础,我们在开发过程中可能很少有机会直接使用到java提供的这两个算法或框架,但是这两种算法确实构成java并发编程的基石。

像我们熟悉的ReentrantLock,信号量,栅栏,CountDownLatch等JUC包下的并发工具类都是由AQS框架实现的。原子类,ConcurrentHashMap等工具包括AQS的实现都是依赖于CAS的。

所以学习CAS和AQS对于学习java并发编程的原理有着重要的意义。

CAS(Compare And Swap)

CAS算法理解:

cas即比较再交换,是一种无锁算法。CAS有三个操作数,内存值V,旧的预期值A,要修改的新值B,当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做

cas可以实现原子性操作

为了理解cas操作,我们先来看一个经典的多线程操作 i++ 的场景

image.png

我们都知道 i操作在计算机底层,是由多条cpu指令来实现的,不是原子操作,想要保证线程安全必须对 i 操作加锁,代码如下:

public class SynchronizedIncrDemo {
 
    private static final int n = 100;
 
    public static void main(String[] args) throws InterruptedException {
        //保证子线程执行完后,主线程再打印storage.i的值
        CountDownLatch cd = new CountDownLatch(n);
        //创建内部类Storage对象
        CASDemo.Storage storage = new CASDemo.Storage();
         
        long t1 = System.currentTimeMillis();
         
        //创建 n 个线程,每个线程内部循环10000 次,进行自增操作
        for (int i = 0; i < n; i++) {
            new Thread(() -> {
                for (int x = 0; x <10000; x++) {
                    storage.incr();
                }
                cd.countDown();
            }).start();
        }
         
        cd.await();
         
        long t2 = System.currentTimeMillis();
        System.out.println(storage.i);
        System.out.println("cost: " + (t2-t1));
    }
 
    static class Storage {
        public  int i;
        //使用 synchronized 加锁实现原子操作
        public synchronized void incr() {
            i++;
        }
    }
}

上面这段代码我们已经非常熟悉了,如果我们使用CAS操作应该怎么用代码实现?

image.png

如图,是cas操作的流程:

  1. 先读取当前i 的值记为 A
  2. 对 i 进行 +1 操作 记为 B
  3. 再读取 i 的值记为 V
  4. 比较 A 和 V是否相等
  5. 相等则更新 i 的值为新值 B
  6. 不相等则重复上述操作

image.png

下面我们用代码来实现

public class CasIncrDemo {
 
    private static final int n = 100;
 
    public static void main(String[] args) throws InterruptedException {
        //保证子线程执行完后,主线程再打印storage.i的值
        CountDownLatch cd = new CountDownLatch(n);
        //创建内部类Storage对象
        Storage storage = new Storage();
 
        long t1 = System.currentTimeMillis();
 
        //创建 n 个线程,每个线程内部循环10000 次,进行自增操作
        for (int i = 0; i < n; i++) {
            new Thread(() -> {
                for (int x = 0; x <10000; x++) {
                    storage.casIncr();
                }
                cd.countDown();
            }).start();
        }
 
        cd.await();
 
        long t2 = System.currentTimeMillis();
        System.out.println(storage.i);
        System.out.println("cost: " + (t2-t1));
    }
 
    static class Storage {
        public  int i;
 
        public void casIncr() {
            int v = i;
            //自旋直到cas操作成功
            while (!compareAndSwap(v, v + 1)) ;
        }
 
        /**
         * cas 操作
         * @param expectedNum 期望值
         * @param newNum 新值
         * @return
         */
        private boolean compareAndSwap(int expectedNum, int newNum) {
            synchronized (this) {
                if (expectedNum == i) {
                    i = newNum;
                    return true;
                }
            }
            return false;
        }
    }
}

上面的cas操作我们从java 语言层面实现了cas 操作,美中不足的是 compareAndSwap 中的操作 我们必须使用 synchronized 来保证原子操作.