CAS(Compare-And-Swap)

CAS

1. CAS 的定义

CAS(Compare-And-Swap)是一种无锁算法,用于实现多线程环境下的原子操作。它的基本思想是:

  1. 检查当前变量的值是否与预期值相同。
  2. 如果相同,则将变量更新为目标值;否则,不进行更新。

在 Java 中,CAS 通常通过 java.util.concurrent.atomic 包中的类实现,例如 AtomicIntegerAtomicReference 等,底层依赖于 CPU 提供的硬件指令(如 x86 架构下的 CMPXCHG 指令)。


2. 乐观锁的定义

乐观锁是一种并发控制策略,假设冲突的可能性较低,因此在操作数据时不加锁,而是在提交更新时检查是否有冲突。如果检测到冲突,则采取重试或其他措施。

乐观锁的核心特点:

  • 不会阻塞线程。
  • 适用于读多写少的场景。
  • 需要某种机制来检测冲突,例如版本号或时间戳。

3. CAS 为什么可以被视为乐观锁

  1. 无锁设计

    • CAS 不需要像 synchronizedReentrantLock 那样显式加锁,而是通过比较和交换的方式尝试更新数据。
    • 这种设计符合乐观锁的理念:假设冲突较少,直接尝试操作。
  2. 冲突检测

    • CAS 的核心机制是 “比较并交换” ,即在更新前检查当前值是否与预期值一致。如果不一致,说明发生了冲突,操作失败。
    • 这种冲突检测机制正是乐观锁的核心特性。
  3. 重试机制

    • 在 CAS 操作失败时,通常会通过循环重试(如自旋锁)来重新尝试更新。这种重试逻辑与乐观锁的冲突处理方式类似。

4. CAS 和乐观锁的区别

尽管 CAS 可以被看作乐观锁的一种实现形式,但两者并不完全等同:

对比维度CAS乐观锁
范围是一种具体的实现机制,基于硬件支持的原子操作。是一种抽象的并发控制策略,不一定依赖 CAS。
冲突处理通常通过自旋重试解决冲突。冲突处理方式多样(如重试、回滚、抛异常)。
适用场景适用于简单的原子操作(如计数器)。适用于更复杂的业务场景(如数据库事务)。
性能开销自旋可能导致高 CPU 占用。冲突较少时性能较高,冲突较多时可能效率低。

5. 示例代码

CAS 实现的乐观锁

以下是一个使用 AtomicInteger 实现 CAS 的例子,模拟乐观锁的行为:

import java.util.concurrent.atomic.AtomicInteger;

public class CASExample {
    private AtomicInteger balance = new AtomicInteger(100);

    public void withdraw(int amount) {
        while (true) {
            int currentBalance = balance.get();
            if (currentBalance < amount) {
                System.out.println("余额不足,无法取款");
                break;
            }
            // CAS 更新余额
            if (balance.compareAndSet(currentBalance, currentBalance - amount)) {
                System.out.println("取款成功,当前余额:" + balance.get());
                break;
            }
            // 如果 CAS 失败,说明有其他线程修改了余额,继续重试
        }
    }

    public static void main(String[] args) {
        CASExample account = new CASExample();
        Runnable task = () -> account.withdraw(50);
        new Thread(task).start();
        new Thread(task).start();
    }
}

数据库中的乐观锁

以下是数据库中使用版本号实现乐观锁的例子:

-- 假设表中有 version 字段
UPDATE accounts
SET balance = balance - 50, version = version + 1
WHERE id = 1 AND version = 2;

如果 version 不匹配,说明数据已被其他事务修改,更新失败。


6. 总结

  • CAS 是乐观锁的一种具体实现,它通过无锁的方式实现了冲突检测和重试机制。
  • 乐观锁是一种更广泛的概念,不仅限于 CAS,还可以通过版本号时间戳等方式实现。
  • 在实际应用中,CAS 更适合简单场景(如计数器、状态标志位),而乐观锁则更适合复杂业务场景(如数据库事务)

因此,虽然可以笼统地将 CAS 视为乐观锁,但需要根据具体场景理解两者的区别和适用范围。

基于 CAS 的实现类

以下是常见的基于 CAS 的实现类及其用途,它们都依赖于 CAS 机制来实现线程安全的原子操作:


1. 基本数据类型的原子类

这些类用于对基本数据类型(如整数、长整型、布尔值等)进行原子操作。

  • AtomicInteger

    • 提供对 int 类型的原子操作。
    • 示例: incrementAndGet()getAndIncrement()compareAndSet(int expect, int update)getAndSet(int newValue)等。
  • AtomicLong

    • 提供对 long 类型的原子操作。
    • 示例:incrementAndGet()getAndSet() 等。
  • AtomicBoolean

    • 提供对 boolean 类型的原子操作。
    • 示例:compareAndSet(true, false) 可以原子地将 true 改为 false
  • AtomicReference<V>

    • 提供对引用类型的原子操作。
    • 示例:可以原子地更新一个对象引用。

2. 数组类型的原子类

这些类用于对数组中的元素进行原子操作。

  • AtomicIntegerArray

    • 提供对 int[] 数组的原子操作。
    • 示例:getAndIncrement(int i) 可以原子地增加数组中某个位置的值。
  • AtomicLongArray

    • 提供对 long[] 数组的原子操作。
  • AtomicReferenceArray<E>

    • 提供对对象数组的原子操作。

3. 带版本号或标记位的原子类

这些类解决了 CAS 中的 ABA 问题(即变量从 A 变为 B 再变回 A 的情况)。

  • AtomicStampedReference<V>

    • 在引用的基础上引入了一个“版本号”字段。
    • 示例:compareAndSet(expectedReference, newReference, expectedStamp, newStamp)
  • AtomicMarkableReference<V>

    • 在引用的基础上引入了一个“标记位”字段。
    • 示例:compareAndSet(expectedReference, newReference, expectedMark, newMark)

4. 高性能计数器

在高并发场景下,传统的原子类(如 AtomicLong)可能会因为自旋重试导致性能下降。为此,Java 提供了以下高性能计数器类:

  • LongAdder

    • 提供高效的加法操作,适用于频繁更新但较少读取的场景。
    • 内部通过分段计数的方式减少竞争。
  • DoubleAdder

    • 类似于 LongAdder,但适用于 double 类型。

5. 其他高级工具

  • Striped64

    • LongAdderDoubleAdder 的基类,提供了分段计数的通用实现。
  • AtomicIntegerFieldUpdater<T>

    • 提供对指定类的 volatile int 字段的原子更新功能。
  • AtomicLongFieldUpdater<T>

    • 提供对指定类的 volatile long 字段的原子更新功能。
  • AtomicReferenceFieldUpdater<T, V>

    • 提供对指定类的 volatile 引用字段的原子更新功能。

CAS 的核心实现方式

所有的上述类都依赖于 Unsafe 类提供的底层 CAS 方法。例如:

  • Unsafe.compareAndSwapInt(Object o, long offset, int expected, int update)
    • int 类型的字段执行 CAS 操作。
  • Unsafe.compareAndSwapLong(Object o, long offset, long expected, long update)
    • long 类型的字段执行 CAS 操作。
  • Unsafe.compareAndSwapObject(Object o, long offset, Object expected, Object update)
    • 对引用类型的字段执行 CAS 操作。

如果你需要处理简单的整数操作,可以选择 AtomicInteger;如果需要高性能计数器,可以使用 LongAdder;如果需要解决 ABA 问题,可以使用 AtomicStampedReference

AtomicInteger源码解读

AtomicInteger 是 Java 并发包 java.util.concurrent.atomic 中的一个类,用于在多线程环境下提供原子操作的整数类型。它的核心实现依赖于 CAS(Compare-And-Swap) 操作,这是硬件级别的支持机制,能够实现无锁的线程安全操作。


1. 基本结构

AtomicInteger 的核心是一个 volatile 修饰的整型变量,确保其可见性和有序性

private volatile int value;
  • volatile:保证了变量的可见性,即一个线程对 value 的修改对其他线程立即可见。
  • int value:存储当前的整数值。

此外,AtomicInteger 还使用了 Unsafe 类提供的底层方法来实现 CAS 操作


2. 核心方法解析

(1) get() 方法

public final int get() {
    return value;
}
  • 直接返回当前的 value 值。
  • 因为 valuevolatile 的,所以读取时总是能获取到最新的值

(2) compareAndSet(int expect, int update) 方法

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
  • 作用:尝试将 valueexpect 更新为 update,如果当前值等于 expect,则更新成功;否则失败。
  • 实现细节
    • unsafe.compareAndSwapInt 是一个底层方法,直接调用 CPU 的 CAS 指令(如 x86 架构下的 CMPXCHG)。
    • valueOffsetvalue 字段在内存中的偏移量,通过 Unsafe 获取。
    • CAS 操作是原子性的,不会被其他线程打断

(3) incrementAndGet() 方法

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
  • 作用:将 value 增加 1,并返回增加后的值。
  • 实现细节
    • 调用了 UnsafegetAndAddInt 方法:
      public final int getAndAddInt(Object o, long offset, int delta) {
          int v;
          do {
              v = getIntVolatile(o, offset); // 获取当前值
          } while (!compareAndSwapInt(o, offset, v, v + delta)); // 尝试更新
          return v;
      }
      
    • 这里使用了一个自旋循环(do-while),不断尝试 CAS 操作,直到更新成功为止。
    • 如果多个线程同时竞争,可能会导致多次重试,但最终总能成功。

(4) getAndIncrement() 方法

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
  • 作用:将 value 增加 1,并返回增加前的值。
  • 实现细节
    • 同样调用了 UnsafegetAndAddInt 方法。

3. 乐观锁的核心思想

AtomicInteger 实现乐观锁的核心在于 CAS 操作自旋重试机制

  1. CAS 操作

    • CAS 是一种无锁算法,基于硬件指令实现。
    • 它通过比较当前值与预期值是否一致来决定是否更新,避免了传统锁的开销。
  2. 自旋重试

    • 当多个线程同时尝试更新同一个 AtomicInteger 时,只有一个线程会成功,其他线程会进入自旋重试。
    • 自旋的优点是无需阻塞线程,减少了上下文切换的开销。

4. 示例代码

以下是一个简单的例子,展示 AtomicInteger 在多线程环境下的使用:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
    private static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet(); // 原子性地增加计数器
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

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

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

        System.out.println("Final Counter Value: " + counter.get());
    }
}

输出结果

Final Counter Value: 2000
  • 两个线程分别对 counter 增加 1000 次,最终结果是 2000,证明了 AtomicInteger 的线程安全性。

5. 优点与局限性

优点

  1. 高性能
    • CAS 操作比传统的锁(如 synchronized)更高效,尤其是在低竞争场景下。
  2. 无锁设计
    • 不需要显式加锁,减少了线程阻塞和上下文切换的开销。
  3. 简单易用
    • 提供了丰富的原子操作方法,简化了并发编程。

局限性

  1. 高竞争场景下的性能问题
    • 在高并发场景下,CAS 可能会导致大量线程自旋重试,增加 CPU 开销。
  2. ABA 问题
    • 如果变量的值从 A 变为 B,再变回 A,CAS 会误认为没有变化。
    • 解决方案:使用 AtomicStampedReferenceAtomicMarkableReference,引入版本号或标记位。

6. 总结

AtomicInteger 通过 CAS 操作自旋重试机制 实现了乐观锁,能够在多线程环境下提供高效的原子操作。它适用于低竞争的场景,但在高竞争场景下可能需要结合其他工具(如锁)来优化性能。理解 AtomicInteger 的工作原理有助于更好地掌握 Java 并发编程的核心思想。

有关于CAS的面试题

CAS(Compare and Swap)是高频面试题的一个热门话题,尤其在多线程和并发编程方面经常被问及。

什么是CAS操作?

CAS是一种乐观锁机制,用于实现多线程环境下的原子操作。
它通过比较共享变量的当前值与期望值是否相等,如果相等则将共享变量的值更新为新值
CAS是一种非阻塞算法,可以避免传统锁机制带来的性能开销和线程阻塞。

CAS操作的基本步骤是什么?

  1. 获取当前共享变量的值期望值
  2. 比较共享变量的当前值和期望值是否相等,如果相等更新为新值
  3. 如果当前值与期望值不相等,说明有其他线程已经修改了共享变量的值,需要重新获取最新值并重复步2

在Java中,CAS操作由哪些类提供支持?

CAS操作由java.util.concurrent.atomic包下的类提供支持,主要包括AtomicIntegerAtomicLongAtomicReference等。这些类提供了原子性的CAS操作方法。

CAS操作在并发编程中有什么优点?

● 确保共享变量的原子性操作,避免了数据不一致的问题。
● CAS是非阻塞的,不会导致线程阻塞和上下文切换,提高了性能
● 它适用于高并发环境,如数据库事务、分布式系统等,提供了高度的并发度。

CAS操作存在哪些问题?

其中最常见的是ABA问题。
ABA问题指的是,一个共享变量的值从A变为B,然后再从B变回A,这样CAS操作可能会错误地认为没有其他线程修改过值
为了解决ABA问题,可以使用带有版本号的CAS操作。

什么是CAS操作的ABA问题?如何避免?

它指的是在CAS操作期间,共享变量的值由A变为B,然后再从B变回A。这可能导致CAS操作错误地认为没有其他线程修改过值。
为了避免ABA问题,可以使用 版本号或标记(一般加时间戳比较保险) 来跟踪共享变量的变化,确保CAS操作同时检查值和版本号。

CAS操作和互斥锁有何不同 ?

CAS操作和互斥锁的主要不同在于:

  1. CAS是一种乐观锁,不会导致线程阻塞,而互斥锁是一种悲观锁,可能导致线程阻塞。
  2. CAS操作是非阻塞的,而互斥锁需要等待资源释放
  3. CAS操作通常用于高并发环境,互斥锁用于临界区的互斥访问

在哪些应用场景中可以使用CAS操作?

CAS操作适用于多种高并发应用场景,包括但不限于:

  1. 数据库事务:用于实现乐观锁机制,避免死锁和性能问题。
  2. 分布式系统:CAS可以用于分布式锁、分布式数据同步等。
  3. 线程安全的数据结构:CAS可用于实现线程安全的队列、栈、集合等数据结构。JDK1.8 中的 ConcurrentHashMap 使用 CAS 和 synchronized 两种机制来实现线程安全。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值