【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 抢占
相关推荐
Allen_LVyingbo2 小时前
《狄拉克符号法50讲》习题与解析(上)
开发语言·人工智能·python·数学建模·量子计算
AC赳赳老秦2 小时前
OpenClaw对接百度指数:关键词热度分析,精准定位博客创作方向
java·python·算法·百度·dubbo·deepseek·openclaw
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(三十)——Direct2D几何体系统:从路径到命中测试
开发语言·c++·windows·信息可视化·c·图形渲染·win32
Ava的硅谷新视界2 小时前
SQLite WAL 模式踩坑笔记:高并发读写下的几个细节
开发语言·后端·编程
小雅痞2 小时前
[Java][Leetcode middle] 274. H 指数
java·算法·leetcode
晚晚不晚2 小时前
分页查询后端实现
java
talen_hx2962 小时前
emqx的Keep alive
java·笔记·学习
huanmieyaoseng10032 小时前
Mybatis常见面试题
java·开发语言·mybatis
xiaoye-duck2 小时前
【C++:C++11】核心进阶:C++11引用折叠、完美转发与可变参数模板实战详解
开发语言·c++·c++11