CAS(Compare-And-Swap)是一种无锁算法,用于在多线程环境下实现原子操作。它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。当且仅当内存位置的值与预期原值相等时,才会将该内存位置的值更新为新值,否则不做任何操作。
本文通过 “排队买奶茶” 的例子,直观地理解 CAS 操作的原理和过程,CAS 操作在多线程环境下可以避免传统锁带来的线程阻塞问题,提高程序的性能和并发处理能力。
场景设定
假如有一家很火爆的奶茶店,店里推出限量版的网红奶茶。大家都想买到这杯奶茶,但是同一时间只能有一个顾客成功购买。这就好比在多线程编程里,多个线程都想对同一个共享资源(限量版奶茶)进行操作(购买),而共享资源在同一时刻只能被一个线程修改。
悲观锁
这种加锁方式下,就好像奶茶店设置了一个排队区域,并且有一个销售(锁)在那里管理。当有顾客想要买奶茶时,需要先问销售能不能进去。如果销售说可以(获取到锁),顾客才能进去买奶茶;在顾客买奶茶的过程中,其他顾客只能在外面排队等待(线程阻塞),直到买奶茶的顾客出来,销售再让下一个顾客进去。这种方式虽然保证了同一时间只有一个顾客能买奶茶,但是其他顾客在等待的过程中什么都做不了,会浪费很多时间。
CAS 操作
CAS 操作就像是另一种更灵活的购买方式。奶茶店门口有一个显示屏,上面显示着当前这杯奶茶的状态(比如 “可购买” 或者 “已售出”),还显示着一个限量版编号(唯一标识编号)。
步骤分析
- 获取预期值:当一个顾客(线程)来到奶茶店,他先看显示屏上奶茶的状态和编号,记住当前的状态是 “可购买”,编号是 123(这就是预期值)。
- 准备新值:这个顾客打算把奶茶买下来,也就是要把显示屏上的状态改成 “已售出”(这就是新值)。
- 比较并交换:顾客拿着自己记住的预期状态和编号,再次去看显示屏。如果显示屏上的状态还是 “可购买”,编号还是 123,说明在他看的这段时间里没有其他顾客买走这杯奶茶。那么他就可以成功地把状态改成 “已售出”,完成购买(这就是比较并交换成功)。但如果显示屏上的状态已经变成 “已售出”,或者编号变了,说明在他看的这段时间里有其他顾客已经买走了这杯奶茶,他这次购买就失败了(比较并交换失败)。这时候,他可以选择再等等,然后重新获取显示屏上的状态和编号,再次尝试购买。
以下是一个简单的 Java 代码示例,用 AtomicInteger
类实现 CAS 操作,类比上面的场景:
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
// 用 AtomicInteger 模拟奶茶的状态,0 表示可购买,1 表示已售出
private static AtomicInteger milkTeaStatus = new AtomicInteger(0);
public static void main(String[] args) {
// 模拟多个顾客(线程)尝试购买奶茶
for (int i = 0; i < 5; i++) {
new Thread(() -> {
int expectedValue = 0; // 预期值:可购买
int newValue = 1; // 新值:已售出
// 尝试进行 CAS 操作
if (milkTeaStatus.compareAndSet(expectedValue, newValue)) {
System.out.println(Thread.currentThread().getName() + " 成功购买到奶茶!");
} else {
System.out.println(Thread.currentThread().getName() + " 购买失败,奶茶已被买走。");
}
}).start();
}
}
}
上述代码中,AtomicInteger
的 compareAndSet
方法就相当于顾客比较显示屏上的状态和编号,并尝试修改状态的过程。只有当预期值和当前值相等时,才会把值更新为新值,否则操作失败。
乐观锁与 CAS 操作的关系
- 联系:CAS 操作是实现乐观锁的一种常见方式。乐观锁的核心思想是在更新数据时检查数据是否被其他线程修改过。
- 区别:乐观锁是一种并发控制的思想,而 CAS 操作是一种具体的实现手段。乐观锁可以通过多种方式实现,如版本号机制、时间戳机制等,而 CAS 操作主要依赖于硬件层面的支持。此外,CAS 操作通常用于解决低级别并发问题,如原子变量的更新(计数器、分布式锁);而乐观锁更侧重于业务层面的数据一致性控制。