Skip to content

第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();
    }
}

读写锁适合读多写少。如果写很多,收益会下降。


📖 四、无锁和并发容器

常见替代:

需求推荐
计数器AtomicLongLongAdder
并发 MapConcurrentHashMap
阻塞队列ArrayBlockingQueueLinkedBlockingQueue
读多写少 ListCopyOnWriteArrayList

高并发计数:

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 deadlock

2. 线程池监控

线程池至少要暴露:

text
activeCount
poolSize
queueSize
completedTaskCount
rejectedCount

队列持续增长说明消费能力不足或下游慢。

3. 锁竞争分析

JFR 可以查看锁竞争事件;Arthas 的 threadtrace 也可以帮助判断方法卡在哪里。


⚠️ 八、常见陷阱

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
线程 APIpthread/std::threadThread、Executor、ForkJoinPool
mutex/rwlock/spinlocksynchronized、Lock、ReadWriteLock
原子操作stdatomic/compiler intrinsicAtomic、LongAdder、VarHandle
内存模型C/C++ memory modelJava Memory Model
工具perf/gdbjstack/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 不高但接口慢等锁、等连接池、等下游
大量 BLOCKEDsynchronized 锁竞争
大量 WAITING队列等待、条件等待、连接等待
拒绝任务增加线程池满,系统进入保护状态
下游雪崩本服务并发过高且没有限流

并发优化必须同时看本服务和下游。只优化本服务吞吐,可能把压力转嫁给数据库或第三方接口。


🔍 十三、自测问题

text
为什么不推荐 Executors.newFixedThreadPool 用于生产?
CPU 密集型和 IO 密集型线程数如何思考?
为什么锁内不能做远程调用?
CompletableFuture 默认线程池有什么风险?
CallerRunsPolicy 的效果是什么?
LongAdder 为什么适合高并发计数?
背压和限流有什么区别?
如何通过 jstack 判断锁竞争?