讲了八百遍,你还是没有理解CAS

📝 ✏️ 📌
讲了八百遍,你还是没有理解CAS

我们是否遇到这样的场景:多个线程同时操作共享变量,如何保证数据的一致性和正确性?传统的锁机制虽然能解决问题,但往往会带来性能瓶颈。今天,我们来聊聊一个更优雅的解决方案——CAS(Compare And Swap)。

什么是CAS?先从一个实际问题说起

假设我们在开发一个电商系统,需要实现一个计数器来统计商品的浏览次数。在高并发场景下,多个用户同时浏览商品,如果简单地使用 count++ 操作,很可能会丢失一部分计数。

// 这样的代码在并发环境下是不安全的

public class ViewCounter {

private int count = 0;

public void increment() {

count++; // 非原子操作,存在竞态条件

}

}

这时候,CAS就派上用场了。CAS是一种无锁的原子操作,它包含三个参数:

内存地址(V):要更新的变量位置

期望值(A):我们认为变量当前应该是什么值

新值(B):想要设置的新值

CAS的核心思想很简单:**"如果变量的值确实是我期望的那个值,那就更新它;否则,说明其他线程已经修改过了,我就不更新了"**。

CAS的工作机制

让我们通过一个具体的例子来理解CAS的工作过程:

基本工作流程

多线程竞争场景

为什么选择CAS?优势和局限性

CAS的优势

在实际项目中,我发现CAS相比传统锁有几个显著优势:

性能更好:没有线程阻塞和唤醒的开销

避免死锁:不存在锁的获取和释放,天然避免死锁

硬件支持:现代CPU都提供了专门的CAS指令

但也要注意这些问题

ABA问题:值从A变成B又变回A,CAS检测不到中间的变化

自旋开销:高竞争时可能会一直重试,消耗CPU

只能保护单个变量:无法实现复杂的原子操作

CAS的底层实现

硬件层面:CPU指令

在x86架构中,CAS操作依赖于 CMPXCHG 指令:

# x86汇编中的CAS指令

CMPXCHG [内存地址], 新值

# 如果 EAX寄存器的值 == [内存地址]的值

# 则 [内存地址] = 新值,并设置ZF标志位

# 否则 EAX = [内存地址]的值,清除ZF标志位

Java中的实现:从Unsafe到现代API

Java早期通过 sun.misc.Unsafe 类提供CAS操作:

// 这是AtomicInteger内部的实现原理

public class AtomicInteger {

private static final Unsafe unsafe = Unsafe.getUnsafe();

private static final long valueOffset;

private volatile int value;

static {

// 获取value字段在对象中的内存偏移量

valueOffset = unsafe.objectFieldOffset

(AtomicInteger.class.getDeclaredField("value"));

}

public final boolean compareAndSet(int expect, int update) {

return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

}

}

实战:自己实现一个CAS计数器

让我们从零开始实现一个线程安全的计数器:

public class CASCounter {

private volatile int count = 0;

private static final Unsafe unsafe = getUnsafe();

private static final long countOffset;

static {

try {

countOffset = unsafe.objectFieldOffset

(CASCounter.class.getDeclaredField("count"));

} catch (Exception e) {

throw new RuntimeException(e);

}

}

public void increment() {

int current;

do {

current = count; // 读取当前值

// 尝试CAS更新,如果失败就重试

} while (!unsafe.compareAndSwapInt(this, countOffset, current, current + 1));

}

public int get() {

return count;

}

}

CAS的实际应用场景

1. 高性能计数器

在我之前的项目中,需要统计API调用次数,最初使用synchronized,后来改为AtomicLong:

public class APICounter {

private final AtomicLong requestCount = new AtomicLong(0);

public void recordRequest() {

requestCount.incrementAndGet(); // 内部使用CAS实现

}

public long getRequestCount() {

return requestCount.get();

}

}

2. 无锁数据结构:栈的实现

CAS的一个典型应用是实现无锁数据结构,比如栈:

public class LockFreeStack {

private volatile Node top;

private static class Node {

final T data;

volatile Node next;

Node(T data) { this.data = data; }

}

public void push(T item) {

Node newNode = new Node<>(item);

Node currentTop;

do {

currentTop = top;

newNode.next = currentTop;

} while (!compareAndSetTop(currentTop, newNode));

}

public T pop() {

Node currentTop;

Node newTop;

do {

currentTop = top;

if (currentTop == null) return null;

newTop = currentTop.next;

} while (!compareAndSetTop(currentTop, newTop));

return currentTop.data;

}

}

3. 缓存更新策略

在缓存系统中,我们经常需要原子地更新缓存值:

public class Cache {

private final ConcurrentHashMap> cache = new ConcurrentHashMap<>();

public boolean updateIfMatch(K key, V expectedValue, V newValue) {

AtomicReference ref = cache.get(key);

return ref != null && ref.compareAndSet(expectedValue, newValue);

}

}

ABA问题及解决方案

ABA问题的真实案例

在一次项目中,我遇到了经典的ABA问题。假设我们有一个链表,两个线程同时操作:

// 问题场景:链表头部的CAS操作

public class LinkedListHead {

private volatile Node head;

public boolean removeHead() {

Node currentHead = head;

if (currentHead == null) return false;

// 在这里,其他线程可能删除了头节点,然后又添加了一个相同的节点

// 导致head看起来没变,但实际上链表结构已经改变

return compareAndSetHead(currentHead, currentHead.next);

}

}

版本号机制

Java提供了 AtomicStampedReference 来解决ABA问题:

public class ABASafeCounter {

private final AtomicStampedReference value =

new AtomicStampedReference<>(0, 0);

public void increment() {

while (true) {

int[] stampHolder = new int[1];

int currentValue = value.get(stampHolder);

int currentStamp = stampHolder[0];

int newValue = currentValue + 1;

int newStamp = currentStamp + 1;

if (value.compareAndSet(currentValue, newValue, currentStamp, newStamp)) {

break;

}

}

}

}

什么时候用CAS?

我做过一个简单的性能测试,比较了synchronized、CAS和LongAdder在不同并发场景下的表现:测试结果(10线程,每线程100万次操作)Synchronized: ~850ms;AtomicInteger: ~320ms ;LongAdder: ~180ms

选择建议

根据我的实践经验:

低并发场景:AtomicInteger等原子类是首选

高并发计数:LongAdder性能更好

复杂业务逻辑:还是用传统锁比较稳妥

无锁数据结构:CAS是核心工具

总结与思考

CAS作为现代并发编程的基石,为我们提供了一种优雅的无锁解决方案。在实际项目中,我发现:

理解原理很重要:知道CAS的工作机制,才能更好地选择和使用

注意使用场景:不是所有情况都适合CAS,要根据具体需求选择

关注性能表现:在高竞争场景下,要考虑使用优化版本如LongAdder

小心ABA问题:在某些场景下,版本号机制是必需的

最后,CAS的学习让我对并发编程有了更深的理解。它不仅仅是一个技术工具,更代表了一种编程思想:通过乐观的方式处理竞争,而不是悲观地加锁等待。

这种思想在分布式系统、数据库设计等领域都有广泛应用,值得我们深入学习和实践。希望这篇文章对大家理解CAS有所帮助。如果你在实际项目中遇到相关问题,欢迎在评论区讨论交流!

🔗 相关推荐

✨ 💡 🎯
用情侣合照当头像的人到底是怎么想的?
建平县—辽宁省—中国—行政区划
365网

建平县—辽宁省—中国—行政区划

📅 01-22 👀 7570
高雄市秝芯旅店駁二館
365网

高雄市秝芯旅店駁二館

📅 10-20 👀 7852