一、当多线程遇上原子操作:从银行转账惨案说起
最近团队里来了个实习生小王,他写了段惊为天人的转账代码:
public class Account {
private int balance;
public void transfer(int amount) {
if (this.balance >= amount) {
//(危险操作)这里可能被其他线程打断
this.balance -= amount;
}
}
}
结果测试时发现:当10个线程同时操作账户时,余额居然出现了负数!!!这个经典案例告诉我们,在并发环境下,看似简单的判断+操作其实存在致命漏洞。要解决这个问题,就得请出今天的主角——CAS(Compare and Swap)。
二、CAS原理大揭秘:CPU级别的原子操作
1. CAS的硬核工作原理(建议拿小本本记)
CAS操作包含三个关键参数:
- 内存地址V
- 旧的预期值A
- 要修改的新值B
执行流程堪比特工行动:
- 潜入内存查看V的真实值
- 如果真实值等于A,立即执行B的替换
- 无论成功与否都带着结果撤退
用代码表示就是:
public class SimulatedCAS {
private int value;
public synchronized int compareAndSwap(int expected, int newValue) {
int oldValue = value;
if (oldValue == expected) {
value = newValue;
}
return oldValue;
}
}
(注:实际CAS由CPU指令直接实现,这里只是示意)
2. ABA问题:你以为的相同其实早已沧海桑田
假设你的银行卡余额是100元:
- 线程A读取余额100,准备转出100
- 线程B先转出100又转回100
- 线程A执行CAS发现余额还是100,成功转出
- 结果:余额变成-100元!!!
解决方法:给数据加上版本号(就像给每次修改盖上时间戳)
三、JUC兵器谱:Java并发编程的瑞士军刀
1. Atomic全家桶(新手必学)
AtomicInteger atomicInt = new AtomicInteger(0);
// 原子性自增
int result = atomicInt.incrementAndGet();
// 自定义CAS操作
atomicInt.updateAndGet(x -> Math.max(x, 100));
2. Lock接口:比synchronized更灵活
ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
// 临界区代码
} finally {
lock.unlock(); //(超级重要)必须放在finally块!
}
// 尝试获取锁(防止死锁的保命技巧)
if (lock.tryLock(1, TimeUnit.SECONDS)) {
// ...
}
3. 并发容器:线程安全的集合库
容器类型 | 线程安全实现 | 适用场景 |
---|---|---|
List | CopyOnWriteArrayList | 读多写少 |
Map | ConcurrentHashMap | 高并发哈希表 |
Queue | LinkedBlockingQueue | 生产者消费者模式 |
四、实战演练:手写高性能计数器
方案1:synchronized版(反面教材)
class SyncCounter {
private int count;
public synchronized void add() {
count++;
}
}
// 性能测试:10线程各执行100万次 → 耗时约520ms
方案2:AtomicLong版(推荐方案)
class AtomicCounter {
private AtomicLong count = new AtomicLong();
public void add() {
count.incrementAndGet();
}
}
// 同样测试条件 → 耗时约120ms(性能提升4倍+)
方案3:LongAdder版(终极杀器)
class LongAdderCounter {
private LongAdder count = new LongAdder();
public void add() {
count.increment();
}
}
// 同样测试条件 → 耗时约65ms(再提升近一倍)
(性能测试环境:MacBook Pro M1,JDK17)
五、避坑指南:来自老司机的血泪经验
- CAS不是银弹:在高竞争场景下,CAS可能导致大量重试,此时应考虑Lock或队列
- 慎用对象引用:AtomicReference存在ABA风险,建议搭配版本号使用
- 锁的粒度控制:能用细粒度锁就别用大锁,就像能用美工刀就别扛青龙偃月刀
- 警惕死锁:按固定顺序获取锁,设置超时时间(tryLock的timeout参数)
- 性能监控:使用JConsole或VisualVM实时监控线程状态
六、扩展思考:从Java到硬件底层
现代CPU如何实现CAS?这涉及到:
- 缓存一致性协议(MESI)
- 内存屏障(Memory Barrier)
- CPU流水线优化
比如Intel处理器的LOCK CMPXCHG
指令,会在执行期间锁住总线,确保操作的原子性。不过随着CPU架构发展,现在的实现方式更智能,会优先尝试在缓存层面完成操作。
七、总结:选择合适工具的智慧
经过这次深度探索,我们可以得出以下决策树:
是否需要原子操作?
├─ 是 → 是否高并发?
│ ├─ 是 → 使用LongAdder
│ └─ 否 → 使用AtomicXXX
└─ 否 → 是否需要互斥?
├─ 是 → 是否需要等待条件?
│ ├─ 是 → 使用Condition+ReentrantLock
│ └─ 否 → 使用synchronized
└─ 否 → 使用volatile
记住:没有最好的并发工具,只有最合适的解决方案。下次当你面对并发难题时,不妨先画个思维导图,再选择合适的JUC组件,相信你一定能写出既安全又高效的多线程代码!