外观
Java 怎样安排 Agent 并发任务,防止堵塞和结果错序?
标准回答
我会先区分任务:UI 更新在主线程,模型网络请求和设备等待属于 I/O,文本处理或本地计算属于 CPU 工作。不同耗时与优先级的任务不宜全挤到一个无界队列里,否则后台长任务可能拖住用户刚提交的操作。
线程池会明确核心线程数、最大线程数、队列容量、超时和拒绝策略,并监控排队时间与执行时间。CPU 任务要避免过量并发,I/O 任务也受连接数、设备服务能力和内存约束,不能无限加线程。参数要根据目标设备和实测调整,不照搬一个固定公式。队列满时可以拒绝、合并过期任务或提示稍后重试,但不能悄悄丢掉用户已经认为受理的操作。
共享状态尽量收口到串行执行器,或者用锁和原子操作保护明确的不变量。volatile 可以提供相应的可见性与有序性,但不能把 count++ 变成原子操作;synchronized 能保护临界区,也建立相应的可见性关系。锁内保持短小,不做网络、推理或长时间 IPC。
每个异步结果都带任务标识或版本。回调到主线程时确认页面仍在观察该任务、结果没有过期,再更新 UI。取消采用协作方式,并同时取消可取消的底层请求;Future.cancel(true) 发出中断请求,不等于任意工作已经停止。
关键机制
- 线程池容量与队列上限共同控制负载;分别观测排队和执行耗时。
- 状态收口或使用适当同步;可见性不等于复合操作原子性。
- 结果核对任务版本与生命周期,取消后拒绝迟到结果启动后续步骤。
通俗例子
连续提交 A、B 两次搜索,B 先完成。页面记录当前 requestId 为 B,A 后续返回时只结束自己的资源,不覆盖 B 的结果。任务版本切换与 UI 接收判断可统一在主线程执行。
追问与回答
为什么线程池设置了很大的 maximumPoolSize,还是只看到少量线程?
ThreadPoolExecutor 在达到核心线程数后通常先尝试入队,队列无法接收时才继续创建到最大线程数。如果用了无界队列,任务可能一直排队,maximumPoolSize 就很难发挥作用。因此需要一起理解核心线程、队列和最大线程数,并用排队时间判断是否拥塞。
volatile 能保证多个线程只创建一个任务吗?
不能。先检查“任务不存在”再创建,是一个复合过程,两个线程仍可能同时通过检查。可以用同一把锁保护检查与写入,或使用合适的原子状态迁移、并发容器操作。需要落盘或跨进程去重时,还要依靠存储层约束,不能只用进程内变量。
调用 cancel(true) 后为什么还会收到回调?
中断是协作信号,任务可能未检查它,底层 I/O 可能有自己的取消方式,也可能在取消前就已完成。所以我会同时处理底层取消和状态层失效:回调仍然核对任务是否有效,必要时只记录已发生结果,不更新旧页面或继续执行。
易混淆点
CallerRunsPolicy 会让提交者执行任务;如果提交者是 UI 主线程,饱和时反而可能把重活放回主线程。Service 也不会自动替你选择后台线程。
参考资料
内容来源
- Android 技术札记 · 原条目
system-concurrency