Java并发:并发工具类、生产消费、背压与优雅停机
前言
并发工具类、队列和停机策略共同决定异步任务在压力下是否可控。本文对比常见协作工具与生产消费模型,说明如何通过有界队列、背压、任务取消和优雅停机,把无限积压和粗暴中断转化为可预期的业务行为。
并发工具类怎么选
| 工具 | 核心语义 | 可否复用 | 生产场景 |
|---|---|---|---|
| CountDownLatch | 等 N 件事完成再继续 | 否 | 并行加载多个启动配置、等待批任务结束 |
| CyclicBarrier | N 个参与者到齐后一起继续 | 是 | 分阶段并行计算、模拟并发压测 |
| Semaphore | 同时最多允许 N 个许可 | 是 | 限制外部 API、文件处理、昂贵资源并发数 |
| Phaser | 可动态注册参与者的多阶段屏障 | 是 | 参与者数量动态的分阶段任务 |
| BlockingQueue | 生产者和消费者之间的有界缓冲 | 是 | 异步批处理、日志/任务削峰 |
| DelayQueue | 到期才可取出的队列 | 是 | 单机延迟任务、超时清理 |
CountDownLatch 适合“主线程等子任务”;CyclicBarrier 适合“子任务互相等”;Semaphore 不是锁,它限制的是并发许可证数量。
有界队列和背压
生产速度大于消费速度时,队列长度会持续增长。无界队列只是在延后失败:内存越来越高、延迟越来越大,最后 OOM。正确做法是让队列有界,并定义满时的业务语义。
flowchart LR
P[生产者] --> Q{有界 BlockingQueue}
Q --> C[消费者]
Q -->|已满| B[阻塞、超时、拒绝、降级或落可靠 MQ]
| 队列满后的选择 | 适合情况 | 风险 |
|---|---|---|
阻塞 put |
上游可以被反压,任务不可丢 | 占满请求线程会导致级联阻塞 |
超时 offer |
允许快速失败或转备用通道 | 必须记录失败并通知调用方 |
| 丢弃 | 指标、可重建日志等 | 关键业务会数据丢失 |
| 转 MQ/持久化任务表 | 关键异步任务 | 需要幂等、重试和可观测性 |
批处理消费
数据库批量写入、文件导入常用“积累到数量或等待到时间就刷一次”的模式。批次不能只追求大:批次过大占内存、锁和事务时间过长;过小则网络和提交开销高。应从数据库连接、SQL 耗时、失败重试粒度和压测结果确定。
消费者处理失败时要区分可重试和不可重试:网络超时可有限重试;格式非法应记录错误并转人工/死信,不要无限重试把队列堵死。
延迟任务与定时任务边界
DelayQueue 在单 JVM 内适合超时关闭、短延迟重试等可丢失或可重建任务。服务重启后队列内数据会消失,集群也会有重复消费/分配问题。订单超时取消、对账补偿等可靠任务应放到数据库任务表、Redis 延迟结构、MQ 延迟消息或分布式调度系统,并用状态机和幂等保证正确性。
优雅停机的消费顺序
- 从负载均衡摘除实例,停止接收新请求。
- 停止接收新任务,关闭生产入口。
- 给消费者限定时间处理在途任务并提交结果。
- 对未完成任务记录进度,依赖幂等在下一实例重试。
- 关闭线程池、连接和客户端资源。
不要以“线程被中断”作为业务完成标志。中断是协作信号,任务代码应检查中断、释放资源,并把可恢复状态持久化。
高频面试题
| 问题 | 回答要点 |
|---|---|
| Latch 和 Barrier 区别? | Latch 是一次性倒计时等待;Barrier 是多方到齐后继续且可复用。 |
| Semaphore 能解决限流吗? | 能限制本 JVM 并发量;分布式全局限流需 Redis/网关等共享方案。 |
| 为什么队列必须有界? | 把过载转化为可见的反压/拒绝,避免无限延迟和 OOM。 |
| DelayQueue 能做订单超时吗? | 单机、重启丢任务,不能单独承担可靠分布式订单超时。 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Junly博客!
评论


