【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 抢占
相关推荐
Albart5753 小时前
Python 实战教程:用 30 分钟学会解决真实问题
开发语言·python
NE_STOP3 小时前
Docker--Docker Swarm集群
java
2301_773643623 小时前
ceph池
开发语言·ceph·python
两年半的个人练习生^_^3 小时前
JMM 进阶:彻底理解 CAS 实现原理
java·开发语言
wuminyu3 小时前
Java锁机制之park和unpark源码剖析
java·linux·c语言·jvm·c++
半个烧饼不加肉3 小时前
JS 底层探究-- 事件循环
开发语言·前端·javascript
W_LuYi1854 小时前
手撸极简zkEVM验证器:RISC-V电路实践
java·risc-v
asdfg12589634 小时前
C 语言中产生伪随机数的标准做法
c语言·开发语言
AI人工智能+电脑小能手4 小时前
【大白话说Java面试题 第102题】【并发篇】第2题:volatile 能否保证线程安全?
java·安全·面试
KobeSacre4 小时前
JUC 概述
java·开发语言