Android架构师必备技能 | 并发编程之操作 CAS

本文深入探讨了Android架构师必备技能之一——并发编程中的CAS(Compare and Swap)原理。CAS是无锁优化的一种方式,是Java并发包的基础。它通过比较并交换内存中的值来实现原子操作,但同时也存在ABA问题、性能问题和只能保证单个变量的原子性等问题。文章介绍了如何使用Atomic类来解决ABA问题,并讨论了锁和CAS在实现原子操作上的差异和选择。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

CAS 的原理

CAS 全称是 compare and swap (比较并且交换),是一种用于在多线程环境下实现同步功能的机制,其也是无锁优化,或者叫自旋,还有自适应自旋。

在 jdk 中,CAS 加 volatile 关键字作为实现并发包的基石。没有 CAS 就不会有并发包,java.util.concurrent 中借助了 CAS 指令实现了一种区别于 synchronized 的一种乐观锁。

乐观锁的一种典型实现机制(CAS):

乐观锁主要就是两个步骤:

  • 冲突检测
  • 数据更新

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

在不使用锁的情况下保证线程安全,CAS实现机制中有重要的三个操作数

  • 需要读写的内存位置(V)
  • 预期原值(A)
  • 新值(B)

首先先读取需要读写的内存位置 (V),然后比较需要读写的内存位置 (V) 和预期原值 (A),如果内存位置与预期原值的 A 相匹配,那么将内存位置的值更新为新值B。如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。

具体可以分成三个步骤:

  • 读取(需要读写的内存位置(V))
  • 比较(需要读写的内存位置(V)和预期原值(A))
  • 写回(新值(B))

CAS 的三大问题

CAS 虽然解决了原子性,但同时它也存在着三大问题。

1)ABA 问题

在 CAS 操作时会先检查值有没有变化,如果没有变化则执行更新操作,如果有变化则不执行更新操作。

假设原来的值为 A,后来更新成了 B,然后又更新成了 A,这个时候去执行 CAS 的检查操作时,内存中的值还是 A,就会误以为内存中的值没有变化,然后执行更新操作,实际上,这个时候内存中的值发生过变化。

那么怎么解决ABA的问题呢?可以在每次更新值的时候添加一个版本号,那么 A->B->A 就变为了 1A->2B->3A,这个时候就不会出现 ABA 的问题了。

在JDK1.5开始,JUC 包下提供了 AtomicStampedReference 类来解决 ABA 的问题。这个类的 compareAndSet() 方法会首先检查当前引用是否等于预期的引用,然后检查当前标志是都等于预期标志,如果都相等,才会调用 casPair() 方法执行更新操作。casPair() 方法最终也是调用了 Unsafe 类中的 CAS 方法。AtomicStampedReference类的compareAndSet() 方法的源码如下。

public boolean compareAndSet(V   expectedReference,
                                V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
   Pair<V> current = pair;
    // 先检查期望的引用是否等于当前引用,期望的标识是否等于当前的标识
    // 然后才执行CAS操作(casPair()也是调用了CAS方法)
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

2)性能问题

CAS 会采用循环的方式来实现原子操作,如果长时间的循环设置不成功,就会一直占用 CPU,给 CPU 带来很大的执行开销,降低应用程序的性能。

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

对一个共享共享变量执行 CAS 操作时,能保证原子操作,但是如果同时对多个共享变量执行操作时,CAS 就无法同时保证这多个共享变量的原子性。这个时候可以使用将多个共享变量封装到一个对象中,然后使用 JUC 包下提供的 AtomicReference 类来实现原子操作。另外一种方案就是使用锁。


实现原子操作

什么是原子操作

原子的意思是说“不能被进一步分割的粒子”,而原子操作是说“不可被终端的一个或多个系列的操作”。假定有两个操作 A 和 B,如果从执行 A 的线程来看,当另一个线程执行 B 时,要么将 B 全部执行完,要么完全不执行 B,那么 A 和 B 对彼此来说是原子的。

java 中可以通过锁,锁机制的方式来实现原子操作,但是有时候需要更有效灵活的机制,synchronized 关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,因为synchronized 关键字具有排他性,如果有大量的线程来竞争资源,那 CPU 将会花费大量的时间和资源来处理这些竞争,同时也会造成死锁的情况。而且锁的机制相当于其他轻量级的需求有点过于笨重,例如计数器,这个后边我会介绍两者之间的性能的比较。

如何实现原子操作

实现原子操作还可以使用CAS实现原子操作,利用了处理器提供的CMPXCHG指令来实现的,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。

CAS 的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事,但是要返回原值是多少。循环 CAS 就是在一个循环里不断的做 CAS 操作,直到成功为止。下面的代码实现了一个 CAS线程安全的计数器 safeCount。

public class Counter {
    private AtomicInteger atomicCount = new AtomicInteger(0);
    private int i = 0;

    /** cas cafecount **/
    private void safeCount() {
        for (; ; ) {
            int i = atomicCount.get();
            boolean suc = atomicCount.compareAndSet(i, ++i);
            if (suc) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        Counter cas = new Counter();
        List<Thread> ts = new ArrayList<>(500);
        long start = System.currentTimeMillis();
        for (int j = 0; j < 100; j++) {
            Thread t = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    cas.safeCount();
                }
            });
            ts.add(t);
        }
        for (Thread t : ts) {
            t.start();
        }
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(cas.i);
        System.out.println(cas.atomicCount.get());
        System.out.println(System.currentTimeMillis() - start);
    }
}

CAS 是怎么实现线程的安全呢?语言层面不做处理,我们将其交给硬件— CPU 和内存,利用 CPU 的多处理能力,实现硬件层面的阻塞,再加上 volatile 变量的特性(可见性,有序性)即可实现基于原子操作的线程安全。

参考:
https://mp.weixin.qq.com/s/oe046IRUbYeXpIpYLz19ew
https://juejin.cn/post/6866795970274394126
https://blog.csdn.net/bugmiao/article/details/110859112
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值