【JAVA基础面经】CAS 与 ABA

文章目录


CAS

 CAS(Compare and swap)的主要工作是将寄存器/某个内存中的值和另外一个内存的值进行比较,如果值相同,就把另一个寄存器/内存的值和当前这个内存进行交换,其伪代码如下所示,

java 复制代码
boolean CAS(address, expectValue, swapValue){
	if (&address == expectValue){
		&address = swapValue;
		return true;//操作成功
	}
	return false;//操作失败
}

  其中,address为待比较的内存地址,expectValue为预期内存中的值,swapValue希望把内存中的值修改成的新值

 一个CAS涉及到下面的操作:1.比较 A 与 V 是否相等。2.如果比较相等,那就将B写入A。3.返回操作是否成功。

  此处所指的CAS指的是CPU提供了一个单独的CAS指令,通过一条指令能够完成上述伪代码描述的过程,相当于是原子的,指令不可分割,保证了线程安全

通过硬件直接实现了上面的交换逻辑,通过这一条指令进行了封装

1.基于CAS能够实现"原子类"

  JAVA标准库中提供了一组原子类,针对常用的int、long、int array等进行了封装,可以基于CAS的方式进行修改,并且线程安全。

  因为代码基于CAS实现的自增操作,因此不存在安全问题,这样的操作既能够保证线程的安全,又能比synchronized高效,因为synchronized会涉及到两个锁之间的竞争,线程之间要相互等待,而CAS则不涉及线程阻塞等待的问题。

  当多个线程同时对某个资源进行 CAS 操作的时候,是能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会获得操作失败的信号,CAS 可以视为一种乐观锁,下面是基于CAS实现的两个线程进行自增的操作,得到的结果是100000,保证了线程的安全。

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger num = new AtomicInteger(0);
        Thread t1 = new Thread(()->{
            for(int i = 0;i < 50000;i++){
                //相当于num++;
                num.getAndIncrement();
            }
        });

        Thread t2 = new Thread(()->{
            for(int i = 0;i < 50000;i++){
                //相当于num++;
                num.getAndIncrement();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        //得到原子类内部的数值
        System.out.println(num.get());
    }
}

 下面是实现原子类的伪代码,此处的 oldValue 在实际实现中是直接用寄存器存储的内容 ,value 是内存中的原始数据,CAS(value,oldValue,oldValue+1) 操作理解为,判定内存中的值和寄存器中的值是否一致,如果一致,就把内存中的value值设置为 oldValue+1,返回 true,循环结束;如果判定失败,就返回 false 将寄存器中的值跟新为内存中的值,再循环进行CAS判定,直到判定成功

java 复制代码
class AtomicInteger{
    private int value;

    public int getAndIncrement(){
        int oldValue = value;
        //两行代码之间可能有其他线程修改了value的值
        while( CAS(value,oldValue,oldValue+1) != true){
            oldValue = value;
        }
        return oldValue;
    }
}

 两个线程分别进行自增操作,其中一种情况的流程如下,

2.基于CAS类能够实现"自旋锁"

 自旋锁伪代码

java 复制代码
public class SpinLock{
    private Thread owner = null;

    public void lock(){
        //通过CSA 看当前锁是否被某个线程持有
        //如果这个锁已经被别的线程持有,那么就自旋等待
        //如果这个锁没有被别的线程持有,那么就把 owner 设置成当前尝试加锁的线程
        while(!CAS(this.owner,null,Thread.currentThread())){
        }
    }

    public void unlock(){
        this.owner = null;
    }
}

 Thread owner记录当前锁被哪个线程持有了,null表示当前未加锁。CAS会比较当前的 owner 是否是 null,如果是 null就改成当前线程,当前线程就拿到了锁,如果不是null就返回 false 进入下次循环,下次循环再进行CAS判定。如果当前锁一直被别人持有,当前加锁的线程就会再while循环处反复,也就是忙等,自旋锁是一个轻量级锁,也可以视为一个乐观锁,认为当前锁虽然没有拿到,但是预期很快就能拿到,短暂的自旋并不会产生大量的CPU损耗。

ABA问题

 CAS的关键是先比较,再交换,在比较的过程中,当前值和旧值是相同时,就认为中间没有发生过改变,但是可能存在中间发生了改变,但多次改变后值不变的情况。

 一个典型的ABA问题的例子,如下图所示,在银行进行取钱时,连续点击了两次取钱100的操作,即线程t1、t2,如果没有 t3 线程那么 t2 线程会判断为 false ,从而取消第二次的重复操作。若在 t2 线程 CAS 操作前 t3 线程对数据进行了 +100 的操作,此时t2线程会判定为true,本来第二次误操作就会执行成功。

9

1.加版本号

 对于ABA问题的解决,可以引入一个"版本号",版本号只能变大,不能变小,修改变量的时候,就不再比较变量的值本身,而是比较版本号,每次对value进行修改的时候,都要对版本进行加一,每次修改前也要检查版本是否相同。也可使用时间戳,与设置版本号的方式类似

 这种基于版本号进行多线程的控制,也是一种乐观锁的体间,常见于数据库管理,版本管理工具(SVN,通过版本号进行多人协同)

2.指针标记

 在一些非数值型的数据结构(如无锁栈、无锁队列)中,仅仅给数值加版本号是不够的,因为操作的对象是指针(内存地址)。此时可以采用 指针标记(Pointer Tagging) 技术。

java 复制代码
class LockFreeStack {
    Node top;           // 栈顶指针
    int version;        // 单独维护的版本号
}

 CPU 的 CAS 指令天然支持对一个机器字长(如 64 位)的值进行原子比较和交换。其末3位永远是0,指针标记就是把版本内嵌到了地址中,使用指针寻址前把它们再清零即可。

  • 对于只维护版本号的方案,需要同时修改 top 和 version。CAS 只能原子地修改一个内存位置,无法用一条 CAS 同时更新两个独立变量。
java 复制代码
// ❌ 这样写是线程不安全的:
Node oldTop = top;
int oldVer = version;
// 中间可能被其他线程修改,然后 CAS 只改了 top,版本号没变!
CAS(&top, oldTop, newTop);
version++;  // 这不是原子的!

面试常见问题

1.CAS有什么缺陷/缺点

  • ABA 问题:变量可能被其他线程修改后又改回原值,CAS 无法感知中间过程的变化。
  • 自旋开销大:如果 CAS 长时间失败,线程会陷入无限循环自旋,白白消耗 CPU 资源(因此 Java 中的原子类底层配合 pause 指令或自适应自旋进行优化)。
  • 只能保证一个共享变量的原子操作:当需要对多个变量同时进行原子更新时,CAS 无法直接胜任(需要使用 AtomicReference 封装对象,或采用锁机制)。
  • 存在"总线风暴"问题:当大量线程在同一个共享变量上高并发执行 CAS 时,由于 CAS 底层通过总线锁或缓存锁保证原子性,会导致总线流量激增,影响性能。

2.synchronized 与 CAS(原子类)的区别?

对比维度 synchronized CAS(原子类)
实现层面 JVM 内置关键字,底层基于 monitor 锁机制 Java 层调用 Unsafe 类,最终依赖 CPU 的 CAS 指令
线程状态 未获取到锁时线程阻塞挂起,进入内核态 失败时循环重试,线程保持在用户态运行
适用场景 临界区代码较长、竞争激烈 临界区极短、读多写少、竞争不激烈
性能表现 重量级锁切换开销大,但阻塞后不占 CPU 竞争激烈时空耗 CPU,可能不如 synchronized
公平性 非公平锁(ReentrantLock 可设为公平) 完全非公平,完全依赖 CPU 抢占
相关推荐
budingxiaomoli2 小时前
Spring统一功能处理
java·后端·spring
eggwyw2 小时前
Anaconda安装卸载的详细流程指南
开发语言·python
Flynn Liang4 小时前
可观测性与成本治理:一次对话的 17 个观测点
java·langchain·可观测性·micrometer·langchain4j·token 成本·llm 监控
金銀銅鐵4 小时前
带您看懂字节码指令(三):同一行加法,为什么有四种指令
java·jvm
another heaven7 小时前
【C/C++ Ranges 是零开销抽象吗?拆开管道、惰性求值和编译期膨胀】
c语言·开发语言·c++
文丑颜不良啊8 小时前
LeetCode2821~2840题目解析
java·leetcode
SomeOtherTime8 小时前
Rust 闭包语法和原理(AI回答)
开发语言·后端·rust
apcipot_rain8 小时前
C++转Java日记(10)
java·开发语言
总要学点什么9 小时前
第105篇:三维特效-流动图片墙体效果——让箭头沿墙长滚,它俩竟是同一份代码
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
简 洁 冬冬9 小时前
泛型方法写法全面辨析:从「能编译」到「能跑」到「能用」
java·泛型