系统,需要实现一个计数器来统计商品的浏览次数。在高并发场景下,多个用户同时浏览商品,如果简单地使用 count++ 操作,很可能会丢失一部分计数。
代码语言:Java
自动换行
AI代码解释
// 这样的代码在并发环境下是不安全的 public class ViewCounter { private int count = 0; public void increment() { count++; // 非原子操作,存在竞态条件 } }
这时候,CAS就派上用场了。CAS是一种无锁的原子操作,它包含三个参数:
- 内存地址(V):要更新的变量位置
- 期望值(A):我们认为变量当前应该是什么值
- 新值(B):想要设置的新值
CAS的核心思想很简单:**"如果变量的值确实是我期望的那个值,那就更新它;否则,说明其他线程已经修改过了,我就不更新了"**。
CAS的工作机制
让我们通过一个具体的例子来理解CAS的工作过程:
基本工作流程
多线程竞争场景
为什么选择CAS?优势和局限性
CAS的优势
在实际项目中,我发现CAS相比传统锁有几个显著优势:
- 性能更好:没有线程阻塞和唤醒的开aly.sibnails.com1销
- 避免死锁:不存在锁的获取和释放,天然避免死锁
- 硬件支持:现代CPU都提供了专门的CAS指令
但也要注意这些问题
- ABA问题:值从A变成B又变回A,CAS检测不到中间的变化
- 自旋开销:高竞争时可能会一直重试,消耗CPU
- 只能保护单个变量:无法实现复杂的原子操作
CAS的底层实现
硬件层面:CPU指令
在x86架构中,CAS操作依赖于 CMPXCHG 指令:
代码语言:Java
自动换行
AI代码解释
# x86汇编中的CAS指令 CMPXCHG [内存地址], 新值 # 如果 EAX寄存器的值 == [内存地址]的值 # 则 [内存地址] = 新值,并设置ZF标志位 # 否则 EAX = [内存地址]的值,清除ZF标志位
Java中的实现:从Unsafe到现代API
Java早期通过 sun.misc.Unsafe 类提供CAS操作:
代码语言:Java
自动换行
AI代码解释
// 这是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计数器
让我们从零开始实现一个线程安全的计数器:
代码语言:Javaaly.mp3team.com
自动换行
AI代码解释
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:
代码语言:Java
自动换行
AI代码解释
public class APICounter { private final AtomicLong requestCount = new AtomicLong(0); public void recordRequest() { requestCount.incrementAndGet(); // 内部使用CAS实现 } public long getRequestCount() { return requestCount.get(); } }
2. 无锁数据结构:栈的实现
CAS的一个典型应用是实现无锁数据结构,比如栈:
代码语言:Java
自动换行
AI代码解释
public class LockFreeStack<T> { private volatile Node<T> top; private static class Node<T> { final T data; volatile Node<T> next; Node(T data) { this.data = data; } } public void push(T item) { Node<T> newNode = new Node<>(item); Node<T> currentTop; do { currentTop = top; newNode.next = currentTop; } while (!compareAndSetTop(currentTop, newNode)); } public T pop() { Node<T> currentTop; Node<T> newTop; do { currentTop = top; if (currentTop == null) return null; newTop = currentTop.next; } while (!compareAndSetTop(currentTop, newTop)); return currentTop.data; } }
3. 缓存更新策略
在缓存系统中,我们经常需要原子地更新缓存值:
代码语言:Java
自动换行
AI代码解释
public class Cache<K, V> { private final ConcurrentHashMap<K, AtomicReference<V>> cache = new ConcurrentHashMap<>(); public boolean updateIfMatch(K key, V expectedValue, V newValue) { AtomicReference<V> ref = cache.get(key); return ref != null && ref.compareAndSet(expectedValue, newValue); } }
ABA问题及解决方案
ABA问题的真实案例
在一次项目中,我遇到了经典的ABA问题。假设我们有一个链表,两个线程同时操作:
代码语言:Java
自动换行
AI代码解释
// 问题场景:链表头部的CAS操作 public class LinkedListHead<T> { private volatile Node<T> head; public boolean removeHead() { Node<T> currentHead = head; if (currentHead == null) return false; // 在这里,其他线程可能删除了头节点,然后又添加了一个相同的节点 // 导致head看起来没变,但实际上链表结构已经改变 return compareAndSetHead(currentHead, currentHead.next); } }
版本号机制
Java提供了 AtomicStampedReference 来解决ABA问题:
代码语言:Java
自动换行
AI代码解释
public class ABASafeCounter { private final AtomicStampedReference<Integer> 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的学习让我对并发编程有了更深的理解。它不仅仅是一个技术工具,更代表了一种编程思想:通过乐观的方式处理竞争,而不是悲观地加锁等待。