Appearance
第71课:并发优化
🎯 学习目标
- 理解并发优化的目标是提升吞吐和降低等待,而不是简单“多开线程”。
- 掌握线程池、锁竞争、无锁结构、异步化、限流和背压的基本思路。
- 能识别死锁、线程池耗尽、阻塞队列堆积、上下文切换过多等问题。
- 能根据 CPU 密集型和 IO 密集型任务选择不同并发策略。
- 能在优化并发时同时保证正确性、稳定性和可观测性。
📖 一、并发优化先问三个问题
看到接口慢,不要第一反应就开多线程。先问:
text
1. 慢在 CPU 计算,还是慢在 IO 等待?
2. 当前瓶颈是线程不够,还是锁/数据库/连接池/下游服务不够?
3. 增加并发后,下游系统是否能承受?如果数据库已经 100% 忙,再增加业务线程只会让排队更严重。
并发优化的核心不是“让更多线程同时跑”,而是:
text
减少等待
减少锁竞争
控制资源占用
让系统在压力下可降级、可排队、可拒绝📖 二、线程池优化
1. 不要直接 new Thread
错误示例:
java
for (Task task : tasks) {
new Thread(() -> handle(task)).start();
}问题:
text
线程数量不可控
上下文切换过多
无法统一监控
无法优雅关闭
异常容易丢失2. 明确线程池参数
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8,
16,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("order-worker-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);关键参数:
| 参数 | 作用 |
|---|---|
| corePoolSize | 核心线程数 |
| maximumPoolSize | 最大线程数 |
| workQueue | 等待队列 |
| RejectedExecutionHandler | 拒绝策略 |
| ThreadFactory | 线程命名和异常处理 |
3. CPU 密集型与 IO 密集型
text
CPU 密集型:线程数接近 CPU 核心数,避免过多上下文切换。
IO 密集型:线程数可适当大一些,因为很多时间在等待网络/磁盘。但最终参数必须通过压测和监控决定。
📖 三、锁竞争优化
1. 缩小锁粒度
错误示例:
java
public synchronized void process(Order order) {
validate(order);
callRemote(order);
updateStatus(order);
}远程调用在锁内,会让其他线程长时间等待。
优化:
java
public void process(Order order) {
validate(order);
callRemote(order);
synchronized (this) {
updateStatus(order);
}
}只把真正需要互斥的共享状态放进锁。
2. 分段锁
java
private final Object[] locks = new Object[16];
public InventoryService() {
for (int i = 0; i < locks.length; i++) {
locks[i] = new Object();
}
}
public void deduct(Long productId) {
Object lock = locks[(int) (productId % locks.length)];
synchronized (lock) {
// 只锁同一分段的商品
}
}这能减少所有商品共用一把锁带来的竞争。
3. 读多写少用读写锁
java
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public Config getConfig() {
rwLock.readLock().lock();
try {
return config;
} finally {
rwLock.readLock().unlock();
}
}
public void updateConfig(Config newConfig) {
rwLock.writeLock().lock();
try {
this.config = newConfig;
} finally {
rwLock.writeLock().unlock();
}
}读写锁适合读多写少。如果写很多,收益会下降。
📖 四、无锁和并发容器
常见替代:
| 需求 | 推荐 |
|---|---|
| 计数器 | AtomicLong、LongAdder |
| 并发 Map | ConcurrentHashMap |
| 阻塞队列 | ArrayBlockingQueue、LinkedBlockingQueue |
| 读多写少 List | CopyOnWriteArrayList |
高并发计数:
java
private final LongAdder qpsCounter = new LongAdder();
public void recordRequest() {
qpsCounter.increment();
}
public long currentCount() {
return qpsCounter.sum();
}LongAdder 在高竞争场景下通常比 AtomicLong 更适合计数。
📖 五、异步化
异步化适合“主流程不需要立刻等待结果”的场景。
示例:
java
public void createOrder(Order order) {
orderRepository.save(order);
eventPublisher.publishEvent(new OrderCreatedEvent(order.getId()));
}监听器异步发送通知:
java
@Async
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
smsClient.sendOrderMessage(event.orderId());
}异步化不是免费午餐,必须考虑:
text
任务丢失怎么办?
失败是否重试?
是否需要幂等?
队列堆积怎么办?
调用方是否需要感知最终结果?关键业务推荐使用消息队列,而不是只靠内存线程池。
📖 六、限流、降级和背压
并发优化不能只追求更高吞吐,还要防止系统被压垮。
1. 限流
text
固定窗口
滑动窗口
令牌桶
漏桶典型场景:
text
登录接口防爆破
短信发送防刷
下单接口防突发流量
第三方接口调用保护2. 降级
当下游不可用时,返回可接受的兜底结果:
java
try {
return recommendationClient.query(userId);
} catch (Exception e) {
log.warn("recommendation fallback, userId={}", userId, e);
return List.of();
}3. 背压
当消费者处理不过来时,生产者必须减速或拒绝,而不是无限堆积。
text
有界队列
拒绝策略
限速生产
批量消费
监控队列长度📖 七、定位并发问题
1. 线程 dump
bash
jstack <pid> > thread.txt重点看:
text
大量 BLOCKED:锁竞争
大量 WAITING:等待队列或条件
大量 TIMED_WAITING:sleep、连接池等待、超时等待
死锁提示:Found one Java-level deadlock2. 线程池监控
线程池至少要暴露:
text
activeCount
poolSize
queueSize
completedTaskCount
rejectedCount队列持续增长说明消费能力不足或下游慢。
3. 锁竞争分析
JFR 可以查看锁竞争事件;Arthas 的 thread、trace 也可以帮助判断方法卡在哪里。
⚠️ 八、常见陷阱
1. 无界队列
java
Executors.newFixedThreadPool(10)底层使用无界队列,流量突增时可能堆积大量任务导致 OOM。生产项目更推荐显式创建 ThreadPoolExecutor。
2. CompletableFuture 默认线程池
java
CompletableFuture.supplyAsync(() -> query());不传 executor 会使用公共 ForkJoinPool,可能和其他任务互相影响。
3. 锁里做 IO
锁内访问数据库、HTTP、Redis 都会放大锁等待时间。
4. 只加线程不加连接池
业务线程从 20 加到 200,但数据库连接池只有 20,结果只是更多线程排队。
🆚 九、Java vs C 对比
| 维度 | C/C++ | Java |
|---|---|---|
| 线程 API | pthread/std::thread | Thread、Executor、ForkJoinPool |
| 锁 | mutex/rwlock/spinlock | synchronized、Lock、ReadWriteLock |
| 原子操作 | stdatomic/compiler intrinsic | Atomic、LongAdder、VarHandle |
| 内存模型 | C/C++ memory model | Java Memory Model |
| 工具 | perf/gdb | jstack/JFR/Arthas |
Java 提供了更高级的并发工具,但也更容易因为线程池、队列和框架默认值误用而产生隐藏问题。
💡 十、最佳实践
- 优先确认瓶颈类型,再决定是否增加并发。
- 生产线程池必须显式配置有界队列、线程名和拒绝策略。
- 锁粒度尽量小,不在锁内做 IO。
- 高竞争计数优先考虑
LongAdder。 - 异步任务必须有失败处理、重试策略和监控。
- 所有队列都应该有长度指标和告警。
- 增加线程数时同步检查数据库、Redis、HTTP 连接池容量。
- 压测中同时观察吞吐、P99、错误率和资源利用率。
🎓 小结
并发优化是资源协调问题。优秀的并发设计不是无限提高并行度,而是在 CPU、线程、连接池、锁、队列和下游系统之间找到稳定平衡。
真正可靠的系统必须能在高峰期排队、限流、降级和拒绝,而不是让所有请求一起拖垮系统。
🧭 十一、并发优化检查清单
并发相关代码上线前,至少检查:
text
1. 线程池是否显式创建,而不是 Executors 快捷方法。
2. 队列是否有界。
3. 线程名是否能在日志和 jstack 中识别。
4. 拒绝策略是否符合业务预期。
5. 异步任务异常是否会被记录。
6. 是否有超时控制。
7. 是否在锁内访问数据库、Redis 或 HTTP。
8. 是否监控 activeCount、queueSize、rejectedCount。
9. 下游连接池容量是否匹配并发数。
10. 高峰期是否有限流、降级或背压策略。线程池命名非常重要:
text
order-worker-1
payment-callback-3
report-export-2比默认的:
text
pool-1-thread-7更容易在线程 dump 中定位问题。
📌 十二、典型故障模式
| 现象 | 可能原因 |
|---|---|
| 线程数暴涨 | 无限制创建线程或线程池参数错误 |
| 队列持续增长 | 消费速度低于生产速度 |
| CPU 不高但接口慢 | 等锁、等连接池、等下游 |
| 大量 BLOCKED | synchronized 锁竞争 |
| 大量 WAITING | 队列等待、条件等待、连接等待 |
| 拒绝任务增加 | 线程池满,系统进入保护状态 |
| 下游雪崩 | 本服务并发过高且没有限流 |
并发优化必须同时看本服务和下游。只优化本服务吞吐,可能把压力转嫁给数据库或第三方接口。
🔍 十三、自测问题
text
为什么不推荐 Executors.newFixedThreadPool 用于生产?
CPU 密集型和 IO 密集型线程数如何思考?
为什么锁内不能做远程调用?
CompletableFuture 默认线程池有什么风险?
CallerRunsPolicy 的效果是什么?
LongAdder 为什么适合高并发计数?
背压和限流有什么区别?
如何通过 jstack 判断锁竞争?