面试常被忽略的问题——CAS

你好,我是吴计可师,一个工作十多年的后端开发,曾就职京东、阿里等多家互联网头部企业。

点击下方👇关注公众号,带你一起复习后端技术,看看面试考点,补充积累技术知识,每天都为面试准备积累

文章可能会比较长,主要解析的非常详解,或涉及一些底层知识,供面试高阶难度用。可以根据自己实际理解情况合理取舍阅读

CAS(Compare-And-Swap,比较并交换)是一种常用的原子操作,用于实现无锁的并发控制,它是一种基于硬件的同步机制。CAS 是一种乐观锁技术,广泛应用于多线程编程中,尤其在高并发场景下,CAS 能够避免传统的加锁机制带来的性能开销。


01
CAS 的基本原理


CAS 操作包括三个操作数

  • 内存位置:要修改的变量(如某个共享变量的内存地址)。

  • 预期值(Expected Value):CAS 操作希望变量当前的值是什么。

  • 新值(New Value):如果内存位置的当前值等于预期值,那么将其更新为新值。

CAS 的执行流程如下:

  • 比较内存位置的当前值和预期值是否相等。

  • 如果相等,则将内存位置的值更新为新值。

  • 如果不相等,则不做任何操作,返回当前内存位置的值,表示操作失败。

CAS 是一种原子操作,即在操作执行过程中不会被线程调度中断,保证了并发环境下的线程安全。


02
CAS 的实现


CAS 操作通常由硬件支持,CPU 提供了专门的指令(如 Intel 的 CMPXCHG 指令)来实现这一操作。在 Java 中,CAS 操作通常由 java.util.concurrent.atomic 包中的原子类(如 AtomicInteger、AtomicReference 等)来实现。常见的 CAS 操作有:

  • AtomicInteger.compareAndSet(expectedValue, newValue):如果当前值等于 expectedValue,则将其更新为 newValue。

  • AtomicReference.compareAndSet(expectedReference, newReference):对引用类型进行原子比较和交换。


03
CAS 操作的例子


public class CASExample {    private static final AtomicInteger atomicInteger = new AtomicInteger(0);    public static void main(String[] args) {        // 假设当前 atomicInteger 的值为 0        int currentValue = atomicInteger.get();  // 获取当前值        int expectedValue = currentValue;        // 假设预期值为当前值        int newValue = 100;                      // 新值为 100        // 进行 CAS 操作        boolean isUpdated = atomicInteger.compareAndSet(expectedValue, newValue);        System.out.println("CAS 操作成功: " + isUpdated);        System.out.println("当前值: " + atomicInteger.get());    }}


在这个例子中,如果 atomicInteger 的当前值是 0,且 expectedValue 为 0,那么 CAS 操作就会成功,并且将 atomicInteger 更新为 newValue(即 100)。如果 atomicInteger 的值在此期间被其他线程修改了,则 CAS 操作失败。


04
CAS 的优势


无锁操作:CAS 是通过硬件支持的原子操作来保证线程安全,不需要加锁,避免了加锁带来的性能开销

高并发性能:由于 CAS 是无锁的,它在多线程竞争时可以高效地执行,适用于频繁访问共享数据的场景。


05
CAS 的缺点


ABA 问题:CAS 操作会判断预期值与当前值是否相等,如果值的变化是先变成另一个值,再恢复到原来的值,CAS 可能会误认为值没有变化,导致逻辑错误。这种问题称为 ABA 问题。

  • 解决方法:可以通过使用带版本号的变量(如 AtomicStampedReference 或 AtomicMarkableReference)来解决 ABA 问题。

自旋阻塞:如果多个线程不断地竞争同一个变量,CAS 操作可能会导致自旋(即反复尝试操作),从而消耗 CPU 资源,特别是在高并发下,性能可能会受到影响。

只能操作单个变量:CAS 只能对一个变量进行原子操作,无法保证多个变量的原子性。在需要保证多个操作的原子性时,仍然需要加锁机制。


06
CAS 的应用


CAS 被广泛应用于 Java 中的并发编程,尤其是在 无锁数据结构并发控制 中。例如:

  • java.util.concurrent.atomic 包中的类:AtomicInteger、AtomicLong、AtomicReference 等,它们都通过 CAS 实现了对单一变量的原子操作。

  • 无锁队列:一些并发数据结构(如无锁链表、无锁队列等)实现了基于 CAS 的并发控制。

  • 并发算法:例如乐观锁(Optimistic Locking)、自旋锁等,CAS 操作常作为这些算法的基础。


07
直击面试

7.1 什么是 ABA 问题?如何解决?

ABA 问题:如果一个变量从 A 变为 B,又从 B 变回 A,CAS 认为值没变,可能导致错误。

解决方法:引入版本号,通过 AtomicStampedReference 或 AtomicMarkableReference 解决。
示例:

AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 1);

7.2 CAS 是如何实现线程安全的?

CAS 通过 CPU 的硬件指令(如 Intel 的 CMPXCHG)实现原子操作。

Java 中通过 Unsafe 类中的 compareAndSwap 方法调用硬件指令完成。


7.3 CAS 和 Synchronized 的区别是什么?

CASSynchronized
无锁机制,基于硬件原子操作实现。基于 JVM 的重量级锁。
性能较高,但需要解决 ABA 问题。性能较低,但没有 ABA 问题。
适合高并发场景,如计数器等原子操作。适合需要确保多操作原子性的场景。


7.4 CAS 操作可以用于哪些场景?

  • 原子变量:如 AtomicInteger、AtomicLong。

  • 高并发队列:如 ConcurrentLinkedQueue。

  • 乐观锁:如数据库的版本控制。

  • 自定义锁:如自旋锁的实现。

7.5 如果线程频繁自旋导致性能问题,该如何优化?

  • 设定自旋次数限制:如尝试一定次数后切换为阻塞锁。

  • 使用 Thread.yield() 或 LockSupport.park() 暂时挂起线程。

  • 调整业务逻辑,减少共享资源竞争。


7.6 如果多线程场景下有多个共享变量,CAS 能否保证线程安全?

  • CAS 不能直接保证多个共享变量的原子性操作。

  • 可以使用锁(如 synchronized 或 ReentrantLock)来保证整体操作的原子性。

  • 或者通过组合数据结构(如将多个变量封装为对象并使用 AtomicReference)。

7.7 CAS 和自旋锁有什么关系?

自旋锁是一种无锁的同步机制,线程尝试不断检查资源状态,直到成功获得锁。

  • CAS 是实现自旋锁的核心原子操作。

class SpinLock {    private AtomicReference<Thread> owner = new AtomicReference<>();    public void lock() {        Thread currentThread = Thread.currentThread();        while (!owner.compareAndSet(null, currentThread)) {            // 自旋等待        }    }    public void unlock() {        Thread currentThread = Thread.currentThread();        owner.compareAndSet(currentThread, null);    }}


7.8 为什么 CAS 比锁机制性能更高?

  • CAS 不涉及线程上下文切换,减少了内核态和用户态的切换开销。

  • 锁机制会导致线程阻塞和唤醒,增加性能开销。

  • 在低锁竞争场景下,CAS 更高效。


今天分享的内容就到这儿,喜欢的朋友欢迎点赞关注!你的关注是我前进的动力

END


扫码关注

一起积累后端知识
不积跬步,无以至千里
不积小流,无以成江海



喜欢此内容的人还喜欢

谈谈id那些事(五)——美团的 Leaf 的ID生成


一个阿里二面面试官必问的问题


谈谈id那些事(三)——阿里巴巴的 TDDL的ID生成


分享面试:mysql数据库索引失效的情况


面试常被忽略的问题——内存区域划分