CAS底层原理

一、初识CAS

CAS( Compare And Swap)是一种并发编程中常用的原子操作,用于实现无锁并发算法。它是一种乐观锁机制,通过比较内存中的值与期望值是否相等来判断是否发生了并发冲突,从而决定是否更新内存中的值。

CAS操作包括三个步骤:

  1. 读取当前值(Read):首先,CAS会读取内存中的当前值,将其与期望值进行比较。

  2. 比较当前值和期望值(Compare):CAS会将读取到的当前值与预期值进行比较。如果两者相等,则表示没有发生并发冲突,可以进行下一步操作;如果不相等,则表示有其他线程修改了内存中的值,CAS操作失败。

  3. 更新值(Swap):如果CAS操作成功,CAS会将新值写入内存中;如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功

CAS操作是原子的,意味着它在执行过程中不会被其他线程中断。它是通过硬件提供的原子指令来实现的,保证了操作的原子性。在Java中,CAS操作由java.util.concurrent.atomic包中的原子类提供支持,如AtomicIntegerAtomicLong等。

CAS的优点在于它避免了使用锁的开销,提供了一种高效的并发控制机制。它适用于一些并发度较高、竞争不激烈的场景。然而,CAS也存在一些限制和注意事项:

  • ABA问题:CAS操作只能判断值是否相等,无法判断值是否被修改过。如果一个值在CAS操作之前被修改为A,然后又被修改为B,最后又被修改为A,那么CAS操作会误判为没有发生并发冲突。为了解决ABA问题,可以使用版本号或标记位等方式来增加额外的判断。

  • 循环时间长开销大:由于CAS操作是在一个循环中不断尝试的,如果并发冲突较多,CAS操作会导致CPU资源的浪费。因此,在使用CAS时需要注意合理设置重试次数或采用其他并发控制机制。

  • 只能保证一个共享变量的原子操作:CAS操作只能针对单个共享变量进行原子操作,无法保证多个共享变量之间的原子性。

二、代码示例

在下面的代码中,我们使用AtomicInteger类来实现CAS操作。AtomicIntegerjava.util.concurrent.atomic包中的一个原子类,它提供了compareAndSet()方法来进行CAS操作。

import java.util.concurrent.atomic.AtomicInteger;

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

    public static void main(String[] args) {
        // 创建两个线程,分别对counter进行自增操作
        Thread thread1 = new Thread(new IncrementTask());
        Thread thread2 = new Thread(new IncrementTask());

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

    static class IncrementTask implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                int oldValue, newValue;
                do {
                    // 读取当前值
                    oldValue = counter.get();
                    // 计算新值
                    newValue = oldValue + 1;
                    // 使用CAS操作尝试更新值
                } while (!counter.compareAndSet(oldValue, newValue));
            }
        }
    }
}

IncrementTaskrun()方法中,我们使用一个循环来不断尝试CAS操作,直到成功为止。首先,我们读取当前的值(旧值),然后计算出新值。接下来,我们使用compareAndSet()方法来尝试更新值,如果旧值与当前值相等,说明没有发生并发冲突,CAS操作成功;如果不相等,说明有其他线程修改了值,CAS操作失败,需要重新尝试。

通过运行上述代码,我们可以看到两个线程同时对counter进行自增操作,而不需要使用锁来保护共享资源。这就是CAS操作的优势之一,它可以实现无锁的并发控制,提高了性能和并发性能。

三、CAS的底层原理

CAS(Compare and Swap)是一种乐观锁技术,用于实现无锁的并发控制。它是多线程编程中常用的一种同步原语。

CAS操作涉及三个主要的操作数:内存地址V,旧的预期值A和新的值B。CAS操作会比较内存地址V中的值与预期值A,如果相等,则将内存地址V的值更新为新值B。整个操作是原子的,即在操作过程中不会被其他线程干扰。

CAS操作的底层原理如下:

  1. 首先,读取内存地址V中的当前值(旧值)。
  2. 比较旧值与预期值A是否相等。如果相等,则说明内存地址V的值没有被其他线程修改,可以进行更新操作。
  3. 如果相等,将内存地址V的值更新为新值B。更新操作是原子的,不会被其他线程干扰。
  4. 如果不相等,说明内存地址V的值已经被其他线程修改,CAS操作失败。此时,可以重新读取内存地址V的当前值,并再次尝试CAS操作,直到成功为止。

CAS操作的底层依赖于Unsafe类来直接调用操作系统底层的CAS指令。Unsafe类是Java中一个特殊的类,它提供了一些底层操作的方法,这些方法通常是使用native修饰的,由系统提供的接口来执行。

Unsafe类中的CAS方法(如compareAndSwapIntcompareAndSwapLongcompareAndSwapObject等)可以直接操作内存,执行CAS操作。这些方法通过调用底层的CAS指令来实现原子性的比较和交换操作。

在Java中,有许多线程安全的类(如ReentrantLockAtomicIntegerAtomicReference等)都使用了CAS操作来实现并发控制。这些类内部会调用Unsafe类中的CAS方法来进行原子操作,以避免使用锁机制带来的性能开销。

在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作

  • AbstractQueuedSynchronizer(AQS框架)

  • AtomicXXX类

四、乐观锁与悲观锁

乐观锁和悲观锁是两种常见的策略。它们采用不同的思路来处理多个线程同时访问共享资源的情况。

悲观锁是一种保守的并发控制策略。它假设在任何时候都会发生冲突,因此在访问共享资源之前会将其锁定,以防止其他线程的干扰。悲观锁的典型例子是传统的互斥锁(如synchronized关键字)。

当一个线程获取到悲观锁后,其他线程必须等待锁的释放才能访问共享资源。这种策略确保了数据的一致性,但也带来了性能开销。因为每个线程都需要等待锁的释放,即使它们之间并不会真正发生冲突。

乐观锁是一种更加乐观的并发控制策略。它假设在大多数情况下并不会发生冲突,因此允许多个线程同时访问共享资源,而不需要显式地加锁。乐观锁的典型例子是CAS(Compare and Swap)操作。

在乐观锁中,当一个线程要更新共享资源时,它首先会读取当前的值,并进行计算和修改。然后,它尝试使用CAS操作将新值写入共享资源。如果写入成功,说明没有其他线程修改过该资源,操作成功。如果写入失败,说明有其他线程修改过该资源,操作失败。

乐观锁的优势在于它避免了使用锁带来的性能开销。因为多个线程可以同时访问共享资源,只有在写入时才会检查是否发生冲突。这样可以提高并发性能。然而,乐观锁也存在一些问题,例如CAS操作的ABA问题和循环时间长等。

例子

假设有一个银行账户,多个用户同时想要进行取款操作。我们使用悲观锁和乐观锁来保证账户余额的正确性。

悲观锁

使用悲观锁的方式是,当一个用户要进行取款操作时,先获取账户的锁,确保其他用户无法同时访问账户。只有一个用户能够成功获取锁,进行取款操作,其他用户需要等待锁的释放。

synchronized (account) {
    // 获取账户锁
    int currentBalance = account.getBalance();
    // 从账户余额中扣除取款金额
    int newBalance = currentBalance - withdrawalAmount;
    account.setBalance(newBalance);
}

在这个例子中,每个用户在进行取款操作时都需要等待账户锁的释放。这样可以确保每个用户的操作是互斥的,不会发生并发冲突。但是,当多个用户同时想要进行取款操作时,只有一个用户能够成功获取锁,其他用户需要等待,导致并发性能较低。

乐观锁

使用乐观锁的方式是,当一个用户要进行取款操作时,不需要获取账户的锁。每个用户都可以直接读取账户的余额,并计算新的余额。然后,用户尝试使用CAS操作将新的余额写入账户。如果写入成功,说明没有其他用户修改过账户余额,操作成功。如果写入失败,说明有其他用户修改过账户余额,操作失败,需要重新尝试。

int currentBalance = account.getBalance();
int newBalance = currentBalance - withdrawalAmount;
while (!account.compareAndSetBalance(currentBalance, newBalance)) {
    // CAS操作失败,重新读取当前余额并尝试更新
    currentBalance = account.getBalance();
    newBalance = currentBalance - withdrawalAmount;
}

在这个例子中,每个用户都可以同时读取账户的余额,并尝试使用CAS操作进行更新。这样可以避免了锁的竞争,提高了并发性能。但是,如果多个用户同时尝试更新账户余额,只有一个用户能够成功,其他用户需要重新尝试,可能会导致一些额外的开销。

综上所述,悲观锁和乐观锁是两种不同的并发控制策略。悲观锁通过加锁来保证操作的互斥性,确保数据的一致性,但会带来性能开销。乐观锁通过乐观的方式进行操作,允许多个线程同时访问共享资源,提高了并发性能,但可能会导致一些额外的开销。根据具体的场景和需求,选择适合的并发控制策略。

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

abcccccccccccccccode

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值