Loading...

面试官:你知道CAS,Compare and swap吗

阅读 ...

聊CAS之前,得先说说写并发代码最头疼的事——多线程同时改一个共享变量,怎么保证线程安全?

最常用的就是加锁,比如synchronized或者Lock,但不管哪种锁,只要涉及到线程阻塞和唤醒,就有性能开销,尤其是高并发场景下,大量线程抢锁,CPU光花在上下文切换上了,很不划算。

那有没有无锁的线程安全方案呢?

有,这就是CAS。

CAS全称Compare-And-Swap,翻译一哈就是比较并交换,是CPU硬件层面支持的原子操作,核心逻辑特别简单,就3个核心值:

  • 要读写的内存地址 V
  • 我们预期的旧值 A
  • 想要更新成的新值 B

它的执行规则只有一条:

只有当内存地址V里的真实值,等于我们预期的旧值A时,才会把V的值更新成B;
否则不执行更新,返回当前V的真实值。

整个操作是原子的,执行中途不会被其他线程打断。

上面扯了那么多官话,好像也不是太明白,那就举个例子吧。

举个例子

我银行卡里有100块钱(内存V=100),现在要转50块出去,预期账户里还是100(旧值A=100),转完之后应该剩50(新值B=50)。
这时候CAS就会先检查账户里是不是真的有100块,如果是,就直接改成50,转账成功;

如果中途被别的线程改了,比如已经被转走了30,只剩70了,和预期的100对不上,就不执行转账,返回当前的70,避免出错。

我们平时用的AtomicIntegerAtomicLong这些原子类,里面的getAndIncrement()incrementAndGet()这些方法,底层全是靠CAS实现的。

它不用加锁,不会阻塞线程,并发量不极端的情况下,性能比重量级锁好太多了。

ABA问题

CAS虽然香,但它不是万能的,其中最经典、面试必问的坑,就是ABA问题。

先说说ABA问题到底是个甚:

线程1读取到内存V的值是A,正要执行CAS更新;

这时候线程2抢到了CPU,把V的值从A改成了B,然后又改回了A;

等线程1回来执行CAS的时候,发现V的值还是A,和自己预期的一样,就顺利完成了更新。

但实际上,这个A已经不是线程1最开始读到的那个A了,中间已经被其他线程改过一轮了,只是最终值又变回来了而已。这就是ABA问题。

再举个真实的例子

我们有一个栈,栈顶元素是A,A的下一个元素是B,现在我们想通过CAS把栈顶换成B,也就是弹出A。

  1. 线程1读取了栈顶的值A,预期旧值是A,要更新成B,这时候线程1被挂起了;
  2. 线程2抢到执行权,把A、B全都弹出栈,然后又把A重新压回栈里,现在栈里只有A,B已经不在栈里了;
  3. 线程1恢复执行,检查栈顶的值,发现还是A,和预期的一样,CAS执行成功,把栈顶换成了B。

问题来了:

B早就被线程2弹出栈了,现在栈里根本没有B这个元素,线程1的操作直接把整个栈的结构搞崩了。

ABA问题的解决方案

主流的解决方案就是版本号(时间戳)机制

具体逻辑也很简单:

给每个变量额外加一个版本号,每次变量被修改,版本号就+1。

CAS操作的时候,不仅要对比变量的值,还要对比版本号,只有两个都和预期的一致,才执行更新。

还是拿刚才的栈例子来说:
初始状态,栈顶是A,版本号是1;
线程1的预期是:值=A,版本号=1,要更新成B;
线程2修改了栈,把A弹出又压回,这时候A的版本号已经变成3了;
线程1回来执行CAS,发现版本号1和当前的3对不上,直接更新失败,完美避开了ABA问题。

本文由 iamxurulin 原创发布,转载请保留原文链接。
最后更新于 2026-08-23 17:21:42
关于作者与文章

本文为 iamxurulin 原创技术文章。如对内容有疑问或建议,欢迎在评论区交流讨论。

Coder_Studio - 记录后端开发、算法与 AI 的成长之路