并发09:并发设计模式

从生产者-消费者到 Worker Thread,再到 Future 模式,系统理解 Java 并发设计模式的设计思想、适用场景和工程细节,包括容量边界、失败处理和关闭流程。

字数 1539 阅读时长 ≈ 5 分钟 2026-5-29 2026-7-12
并发09:并发设计模式

并发设计模式解决什么问题

并发设计模式经常被讲成名词:生产者-消费者、Worker Thread、Future。项目里真正重要的不是背名字,而是看清楚它们分别在解决哪类压力。

一个典型场景是订单创建后要发短信、写操作日志、同步搜索索引。如果都放在请求线程里同步做,用户会被非核心动作拖慢。于是我们把任务交给后台线程处理。

但”扔到队列里异步执行”只是第一步。队列多大、满了怎么办、失败怎么重试、服务关闭时任务怎么收尾,这些才决定它能不能上线。

生产者-消费者模式

生产者负责提交任务,消费者负责处理任务,中间用队列解耦。

进程内异步模式的完整边界

BlockingQueue<Task> queue = new ArrayBlockingQueue<>(1000);

// producer
queue.offer(task, 100, TimeUnit.MILLISECONDS);

// consumer
while (running) {
    Task task = queue.take();
    handle(task);
}

这个模式的核心不是队列本身,而是容量边界。

队列类型特点风险
无界队列最省事任务无限堆积,拖垮内存
有界队列迫使设计边界需要处理满队列情况

有界队列会迫使你回答业务问题:队列满了是拒绝、降级、同步执行,还是写入可靠消息队列?这些选择不能由技术默认值决定。

背压机制

生产者-消费者模式最怕”生产者永远成功”。如果生产速度持续大于消费速度,系统迟早要在某个地方失败。成熟的设计会让压力尽早反馈给生产者:

  • offer 带超时,超时后返回失败
  • 队列水位超过阈值时入口限流
  • 非核心任务直接丢弃或合并
  • 核心任务转入可靠消息队列,后续补偿

这就是背压。它不是让用户永远等,而是让上游知道下游已经处理不过来,从而降速、降级或改变路径。

Worker Thread 模式

Worker Thread 模式把任务交给固定数量的工作线程。它的价值是限制同时执行的任务数量,避免每来一个请求就创建一个线程。

任务队列 -> worker-1
        -> worker-2
        -> worker-3

Java 里的线程池就是这个模式的常见落地。

这里最容易踩坑的是把不同性质的任务混在一个 worker 池里。报表导出、短信发送、订单核心处理如果共用线程池,报表一慢,订单也会被拖住。

所以 Worker Thread 要配合资源隔离:

  • 核心任务和非核心任务分池
  • 慢 IO 和 CPU 计算分池
  • 重任务要有单独并发上限

Future 模式

Future 模式解决的是”任务异步执行,但调用方未来还要拿结果”。

Future<Result> future = executor.submit(this::queryRemote);
Result result = future.get(300, TimeUnit.MILLISECONDS);
特性说明
获取结果get() 阻塞等待,get(timeout) 带超时
取消任务cancel() 尝试取消
异常处理异常包装成 ExecutionException

但 Future 也容易被写坏:

  • 在请求线程里提交异步任务,然后立刻 get(),没有并行收益
  • 不设超时,远程调用卡住后,请求线程和工作线程一起被拖住

如果有多个异步结果要编排,CompletableFuture 会比普通 Future 更适合;如果只是提交后台任务不关心结果,Future 反而可能是多余复杂度。

失败处理

后台任务失败后,不能只打印日志。至少要区分三种失败:

失败类型特点处理方式
临时失败下游超时、连接抖动延迟重试
永久失败参数错误、状态非法不重试,记录死信
不确定失败请求超时但下游可能成功查询状态或依赖幂等

如果任务有副作用,比如发券、扣库存、推送消息,重试前必须设计幂等键。否则 worker 重启、队列重投、手工补偿都可能制造重复执行。

进程内队列还要面对进程崩溃。只要任务不能丢,就不要只放内存队列;可以用数据库任务表、MQ、延迟队列或调度平台,让任务状态可恢复。

关闭流程

并发模式上线后,服务关闭不是直接 kill 进程。队列里可能还有任务,worker 可能正在处理一半。

一个基本关闭流程应该包括:

  1. 停止接收新任务
  2. 等待队列里已有任务处理一段时间
  3. 超时后取消或转存未完成任务
  4. 记录失败任务,方便补偿

线程池里对应的是 shutdown()awaitTermination()、必要时 shutdownNow()。但更重要的是业务语义:短信可以丢吗,索引同步可以补吗,订单状态变更能不能中断?

常见误区

误区一:用了队列就一定削峰

队列只能缓冲,不能提高消费者处理能力。峰值超过容量太久,队列只是在延迟失败。

误区二:异步任务失败没人管

后台异常不会自动反馈给用户,如果没有日志、重试、死信或补偿,它就会静默丢失。

误区三:把并发模式当成架构方案

进程内队列只能保护当前 JVM,服务重启任务就可能丢。需要可靠投递时,要用数据库任务表、MQ 或可恢复的调度系统。

项目里怎么落地

设计一个异步处理链路时,至少写清楚:

  1. 生产速度和消费速度分别是多少
  2. 队列容量是多少,满了怎么处理?
  3. worker 数量由什么资源决定
  4. 任务失败后重试几次,是否需要死信?
  5. 服务重启时未完成任务怎么恢复
  6. 是否需要把结果返回给调用方

进程内 vs 可靠方案

场景进程内方案可靠方案
本地指标聚合-
短期缓存刷新-
非关键预热-
订单超时关单-
发券、扣减库存-
支付回调后续处理-

一个简单判断是:服务重启时,这个任务丢了会不会影响业务正确性?如果答案是会,就要从进程内模式升级到数据库任务表、MQ 或调度平台。