Lovi.space

Java 后端

Java 并发:从线程池参数回到任务生命周期

批量诊断任务不是“多开线程”就会更快;先定义任务如何排队、超时、取消与降级。

·1 min read
#Java#并发#JUC#线程池

核心洞察:并发设计讨论的主体应是任务生命周期,而不是线程数量。线程池只是把资源限制落实到执行层的工具。 霓虹港每天夜间汇总设备振动、温度和维修记录。最初实现为每台设备提交一个异步任务,白天数据量增长后,队列堆积、超时任务仍在运行、下游数据库连接被占满。问题不在于“线程不够”,而在于任务没有边界。

先建立 Java 内存模型的最低认知

synchronized 提供互斥与可见性;volatile 只保证可见性和一定的有序性,不保证复合操作原子性;CAS 适合短小无阻塞的状态变更;AQS 则是锁、信号量、倒计时门闩等同步器的共同骨架。 不要因为 volatile 看起来轻量就用它保护计数器。count++ 包含读、改、写三个步骤,多个线程仍会覆盖彼此的结果。这里需要 AtomicLong 或锁。

线程池是隔离策略

线程池的四个参数应从工作负载推导:核心线程承接稳定负载,最大线程应受 CPU、连接池和下游容量约束,队列决定背压发生在哪里,拒绝策略定义系统在过载时如何失败。

ExecutorService diagnostics = new ThreadPoolExecutor(
8, 16, 30, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
new ThreadPoolExecutor.CallerRunsPolicy());

有限队列比无界队列诚实。无界队列会把“系统过载”伪装成“稍后再完成”,最终换来内存增长和不可预测的超时。CallerRunsPolicy 会把压力回推调用者,但只适合调用者可被阻塞的场景。

用 CompletableFuture 编排,不要吞异常

批处理往往包含并行读取、合并和单项降级。应给每个阶段定义超时与失败语义:数据源失败是跳过、重试还是终止整批?取消后是否还允许副作用执行?这些问题比 API 链式写法更重要。

CompletableFuture<Result> future = CompletableFuture
.supplyAsync(() -> diagnose(deviceId), diagnostics)
.orTimeout(3, TimeUnit.SECONDS)
.exceptionally(error -> Result.degraded(deviceId, error));

exceptionally 不是“掩盖错误”,而是显式把异常转成领域可理解的降级结果。日志、指标与告警仍必须保留原始异常。

背压与资源隔离

诊断任务、通知任务和 Web 请求不应共享同一个线程池。不同优先级、不同下游依赖的任务需要独立舱室;否则低价值批处理就能挤占高优先级告警。 同时观察活跃线程、队列长度、拒绝数、任务耗时分位数和下游连接池等待。只盯 CPU 会漏掉最常见的阻塞:网络、数据库和锁竞争。

结语

正确的并发不是“让所有任务同时跑”,而是即使任务变多、依赖变慢、局部失败,系统仍能在可预期的时间与资源预算内给出结果。生命周期清晰,线程池参数才有意义。

读到这里,说明你也在认真对待这个问题。

讨论与反应

⌘ K