小白都能看懂的CAS基本原理与实战应用指南

2023-07-08 17:17:41 浏览数 (2)

引言

最近有个读者问我什么是CAS,今天了不起来聊聊CAS(Compare And Swap)这个概念。

它在日常编程中非常有用,特别是在多线程编程中。

本篇文章将从原理介绍、源码分析、实战应用等方面进行阐述。

一、CAS原理介绍

CAS(Compare And Swap)是一种用于实现无锁并发算法的技术。

大多数时候它也还有其他叫法:无锁优化、自旋、乐观锁

它的核心思想是:通过比较当前需要修改的值与预期原来的值,如果相等,则使用新值进行替换。

这个过程是原子性的,它底层是靠C语言依赖的操作系统的原子操作来保证原子性的,即在这个过程中不会被其他线程打断。

在Java中,CAS操作主要是通过 java.util.concurrent.atomic包中的原子类来实现的,如 AtomicIntegerAtomicLong等。

原理示例

假设有一个整数变量 count,初始值为0。现在有两个线程A和B同时对 count进行加1操作。使用CAS操作可以确保 count最终的值为2。

  1. 线程A读取 count的值,得到0。
  2. 线程B读取 count的值,得到0。
  3. 线程A将 count的值与预期值0进行比较,相等,则将 count的值更新为1。
  4. 线程B将 count的值与预期值0进行比较,不相等(因为已经被线程A更新为1),则重试。
  5. 线程B重新读取 count的值,得到1。
  6. 线程B将 count的值与预期值1进行比较,相等,则将 count的值更新为2。

最终,count的值为2,实现了无锁并发操作。

二、源码分析

java.util.concurrent.atomic.AtomicInteger为例,这个类提供了一个原子的整数值,可以用于实现无锁的整数操作。我们来看一下它的 compareAndSet方法的源码:

代码语言:javascript复制
public final boolean compareAndSet(int expect, int update){
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

这里的 unsafe是一个 sun.misc.Unsafe类型的对象,它提供了底层的CAS操作。valueOffset是一个静态常量,表示 AtomicInteger内部的 value字段的内存偏移量。expect是预期值,update是新值。这个方法会返回一个布尔值,表示操作是否成功。

小结:

atomicXXX类
  • Compare and Set/Swap 比较并且设定
  • cas(V,Expected,NewValue)

if V == E

V == New otherwise try again or fail

  • CPU原语支持 atomic底层调用到UnSafe这个方法,三个参数V就是当前版本值,Expected期望值,NewValue赋予的新值。 只有当V等于E的时候才将新的值赋给这个变量,又因为它是原语支持是CPU级别的,是一个原子操作,所以在设值时不会有其他线程来插队设值。

三、实战应用

我们来看一个实际的例子,如何使用 AtomicInteger实现一个无锁的计数器。

代码语言:javascript复制
public class Counter {
    /*volatile*/ //int count1 = 0;

    AtomicInteger count = new AtomicInteger(0);

    /*synchronized*/ void m() {
        for (int i = 0; i < 10000; i  )
            //if count1.get() < 1000
            count.incrementAndGet(); //count1  
    }

    public static void main(String[] args) {
        Counter t = new Counter();

        List<Thread> threads = new ArrayList<Thread>();

        for (int i = 0; i < 10; i  ) {
            threads.add(new Thread(t::m, "thread-"   i));
        }

        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);

    }

}

在这个例子中,我们创建了一个 Counter类,内部使用 AtomicInteger实现了无锁的计数,然后我们创建了10个线程,分别对计数器进行增加操作。最后我们输出计数器的值,可以看到它确实是10000,证明我们的无锁计数器是正确的。

四、CAS在日常中的应用场景

在实际开发中,我们可能会遇到以下几种使用CAS的场景:

  1. 无锁计数器 :如上面的例子所示,我们可以使用CAS实现一个高效的无锁计数器,避免了使用同步锁带来的性能开销。
  2. 单例模式 :我们可以使用CAS实现一种线程安全的单例模式,确保在多线程环境下只创建一个实例。
  3. 并发容器 :在实现高性能的并发容器时,如 ConcurrentHashMap,我们可以使用CAS操作来实现无锁或低锁的数据结构更新。
  4. 多线程并发任务 :在多线程并发执行任务时,我们可以使用CAS操作来确保任务状态的正确更新,例如实现一个无锁的任务分发器。

五、ABA问题

如果另一个线程修改V值,假设原来是A,先修改成B,再修改回成A。

当前线程的CAS操作无法分辨当前V值是否发生过变化,这个就是ABA问题。

解决:

  • 在CAS的时候加版本号,每次操作比较下版本号
  • 加 version
  • A 1.0
  • B 2.0
  • A 3.0
  • cas(version)
  • 原子类 AtomicStampedReference解决ABA问题
  • ABA问题重要不?如果是基本数据类型结果没影响,如果是引用对象就不好说了,比如你的女朋友和你复合,前面经过了多少个其他XXX,你觉得有影响不?

六、总结

了不起带着大家从原理介绍、源码分析、实战应用等方面讲解了CAS的相关知识。

通过本文的学习,相信你们已经对CAS有了一定的了解,掌握了如何在实际开发中应用CAS来解决并发问题。

当然,CAS并不是万能的,它也有一定的局限性,例如ABA问题。

在实际开发中,我们需要根据具体场景选择合适的并发策略。

希望本文能对大家有所帮助,和了不起一起持续学习。

我是了不起
和我一起学习更多精彩知识!!!

0 人点赞