分布式、高并发-Day04
以下是 Day 4 详细学习内容(CAS 与原子操作实战,30 分钟完整计划),包含原理解析、分步代码实战和性能对比:
📖 今日学习目标
- 掌握 CAS(Compare-And-Swap)无锁算法的核心原理
- 学会使用AtomicInteger实现线程安全的计数器
- 理解 ABA 问题及解决方案(AtomicStampedReference)
⏰ 时间分配
时间段 | 任务 | 详细内容 |
---|---|---|
0-10 分钟 | 理论:CAS 原理与原子类 | 1. CAS 三要素:内存值 (V)、旧值 (A)、新值 (B)2. AtomicInteger底层实现3. ABA 问题演示与解决 |
10-25 分钟 | 实战:无锁计数器对比实验 | 1. 用 CAS 实现线程安全计数器2. 用synchronized实现同步计数器3. 对比两种方式的性能与资源占用 |
25-30 分钟 | 总结与扩展 | 1. 记录 CAS 的优势与适用场景2. 思考:为什么 CAS 比synchronized更高效?3. 扩展:如何用 CAS 实现自定义原子操作? |
🔍 理论详解:CAS 核心概念
- CAS 是什么?
- 全称:Compare-And-Swap(比较并交换)
- 核心逻辑:
- 仅当内存中的值 V 等于预期旧值 A 时,才将其更新为新值 B,否则不做任何操作。
// CAS伪代码
boolean cas(V, A, B) {if (V == A) {V = B; // 原子操作,由CPU硬件保证return true;}return false;
}
**优势:**无锁编程,避免线程阻塞(上下文切换损耗),适合竞争不激烈的场景。
2. ABA 问题与解决方案
- 问题场景:
- 线程 1 读取值为 A,线程 2 将其改为 B,再改回 A,线程 1 执行 CAS 时发现值还是 A,误以为未被修改。
- 解决方案:
- 使用AtomicStampedReference,在值之外附加一个版本号(时间戳),每次修改版本号递增:
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
int stamp = ref.getStamp(); // 获取初始版本号
ref.compareAndSet(100, 200, stamp, stamp + 1); // 版本号必须匹配才成功
- 原子类家族
类名 | 作用 | 底层实现 |
---|---|---|
AtomicInteger | 整数原子操作 | Unsafe 类的 CAS 方法 |
AtomicLong | 长整型原子操作 | 同上 |
AtomicReference | 对象引用原子操作 | CAS 操作对象地址 |
💻 实战步骤:无锁计数器对比实验
- 实验 1:用 CAS 实现计数器(AtomicInteger)
import java.util.concurrent.atomic.AtomicInteger;public class CASCounter {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet(); // 底层调用CAS}public int getCount() {return count.get();}public static void main(String[] args) throws InterruptedException {CASCounter counter = new CASCounter();Thread[] threads = new Thread[10];for (int i = 0; i < 10; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < 10000; j++) {counter.increment();}});}for (Thread t : threads) {t.start();}for (Thread t : threads) {t.join();}System.out.println("CAS计数器结果:" + counter.getCount()); // 预期:100000}
}
- 实验 2:用synchronized实现同步计数器
public class SynchronizedCounter {private int count = 0;public synchronized void increment() {count++;}public int getCount() {return count;}public static void main(String[] args) throws InterruptedException {SynchronizedCounter counter = new SynchronizedCounter();Thread[] threads = new Thread[10];for (int i = 0; i < 10; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < 10000; j++) {counter.increment();}});}for (Thread t : threads) {t.start();}for (Thread t : threads) {t.join();}System.out.println("Synchronized计数器结果:" + counter.getCount()); // 预期:100000}
}
- 性能对比(选做,5 分钟)
- 添加计时代码:
long start = System.currentTimeMillis();
// 执行计数逻辑
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");
- 预期结果:
- CAS 版本:耗时更短(无锁,减少上下文切换)
- Synchronized 版本:耗时较长(锁竞争导致线程阻塞)
📝 今日总结与扩展
- 核心知识点速记
特性 | CAS(AtomicInteger) | synchronized |
---|---|---|
锁机制 | 无锁(乐观锁) | 有锁(悲观锁) |
适用场景 | 低竞争场景(如计数、状态标记) | 高竞争场景(如临界资源保护) |
性能 | 高(减少线程阻塞) | 低(可能触发锁升级) |
ABA 问题 | 需AtomicStampedReference解决 | 无(锁保证原子性) |
- 扩展思考(5 分钟)
- 问题 1:CAS 操作会导致 “忙等待” 吗?
答案:是的,CAS 失败时会循环重试(自旋),如果长时间不成功,反而比synchronized更耗 CPU。 - 问题 2:生产环境如何选择 CAS 还是锁?
提示:
读多写少、竞争低:优先用 CAS(如计数器、配置开关)
写多读少、竞争高:优先用synchronized或ReentrantLock
🔧 工具与环境准备
代码要求:直接复制两个 Java 文件,无需额外依赖(JDK 1.8+)
运行验证:
确保两个程序输出均为 100000(线程安全验证通过)
观察控制台是否有异常(如 ABA 问题未解决时可能出现数据错误)
✅ 今日任务 checklist
✅ 理解 CAS 的 “比较 - 交换” 核心逻辑
✅ 成功运行两个计数器程序,验证线程安全性
✅ 记录 1 个 CAS 应用场景(如:分布式系统中的版本号控制)