文章目录
当多线程遇见原子操作(灵魂拷问)
各位老铁!咱们在写多线程程序时,最头疼的就是共享数据读写问题(敲黑板)。举个栗子🌰:当10个线程同时要给同一个银行账户余额+1000元,如何保证最终结果正确?这时候就需要传说中的原子操作登场了!
传统方案翻车现场
// 典型线程不安全示例(千万别在生产环境用!)
private int balance = 0;
void deposit() {
balance++; // 这货不是原子操作!
}
这个简单的balance++
操作,实际上包含了:
- 读取当前值到寄存器
- 寄存器+1
- 写回内存
(重点来了)这三个步骤中间随时可能被其他线程打断,导致最终结果远小于10000!
初探CAS魔法原理
什么是CAS?
Compare and Swap(比较并交换)是CPU级别的原子指令,Java通过Unsafe
类封装了这个操作。其伪代码逻辑:
// 伪代码示意(实际由硬件实现)
public synchronized boolean compareAndSwap(int expected, int newValue) {
if (当前值 == expected) {
当前值 = newValue;
return true;
}
return false;
}
CAS实战三部曲
以AtomicInteger
为例:
- 获取当前值
oldValue
- 计算新值
newValue = oldValue + 1
- CAS操作:如果当前值仍是oldValue,则更新为newValue
(划重点)整个过程不需要加锁,但能保证原子性,这就是CAS的神奇之处!
CAS的AB面:优势与隐患
优势清单(适合收藏)
- ✅ 无锁并发,性能吊打synchronized
- ✅ 避免线程上下文切换开销
- ✅ 死锁?不存在的!
隐藏的ABA问题(注意啦!)
假设账户余额变化:100 → 200 → 100
- 线程A读取到100
- 线程B扣款100 → 存款100
- 线程A执行CAS(100, 200) → 成功!
(发现问题了吗?)虽然余额数值没变,但中间发生过变动,这就是ABA问题。解决方案:
// 使用带版本号的AtomicStampedReference
AtomicStampedReference<Integer> balanceRef =
new AtomicStampedReference<>(100, 0);
// 更新时检查版本号
balanceRef.compareAndSet(100, 200, stamp, stamp+1);
JUC组件实战指南
1. 原子类三剑客
AtomicInteger
:计数器神器AtomicReference
:对象原子更新LongAdder
:高并发统计首选(比AtomicLong更高效)
// 原子类使用示例(线程安全!)
AtomicInteger counter = new AtomicInteger(0);
void safeIncrement() {
counter.getAndIncrement(); // 底层基于CAS
}
2. 锁的艺术品:ReentrantLock
比synchronized更灵活的重入锁:
ReentrantLock lock = new ReentrantLock();
void criticalSection() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须放在finally块!
}
}
(重点功能):
- 可设置公平/非公平锁
- 支持tryLock()超时获取
- 条件变量Condition的精细控制
3. 并发集合之王:ConcurrentHashMap
面试必考题的终极解决方案:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 线程安全的putIfAbsent
map.putIfAbsent("key", 1);
// 分段锁提升并发度(JDK8后采用CAS+synchronized优化)
性能优化Tips(压箱底干货)
- CAS自旋次数:默认10次,可通过
-XX:PreBlockSpin
调整 - 缓存行优化:使用
@sun.misc.Contended
避免伪共享 - 锁升级策略:偏向锁→轻量级锁→重量级锁的优化过程
- 线程池配置:根据任务类型选择不同队列(ArrayBlockingQueue vs LinkedTransferQueue)
避坑指南(血泪教训)
- 🚫 不要过度使用CAS(适合简单原子操作,复杂逻辑用锁)
- 🚫 volatile不能保证原子性(只能保证可见性)
- 🚫 慎用System.out.println调试多线程程序(会改变执行时序)
- 🚫 避免在循环中频繁CAS(可能导致CPU飙高)
实战:手写简易版线程安全缓存
public class ConcurrentCache<K,V> {
private final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public V get(K key) {
rwl.readLock().lock();
try {
return map.get(key);
} finally {
rwl.readLock().unlock();
}
}
public void put(K key, V value) {
rwl.writeLock().lock();
try {
map.put(key, value);
} finally {
rwl.writeLock().unlock();
}
}
}
总结与展望
CAS与JUC组件就像多线程世界的瑞士军刀,但切记:
- 没有银弹!根据场景选择最合适的并发方案
- JDK8的StampedLock、JDK15的Virtual Threads等新特性值得关注
- 推荐《Java并发编程实战》作为延伸阅读(经典永不过时)
老铁们看完别光收藏,赶紧打开IDE写几个demo试试!遇到坑欢迎评论区交流,咱们一起在并发编程的路上升级打怪~