Android 线程池使用指南:从任务提交到生命周期管理
发布时间:2026/9/27 3:16:40 作者:尧图编辑部 阅读量:1,286

打开一个列表页面需要同时解析图片、读取本地缓存、计算缩略图。最直接的写法是为每项创建一个Thread但列表快速滚动后几十个任务可能同时运行CPU 争用、内存上涨页面离开后任务仍在工作。线程池的作用是控制并发和等待任务的数量并复用工作线程。它并不会自动让任务更快配置不当的线程池同样会让页面卡顿甚至造成内存溢出。本文以 Java/Kotlin 的ThreadPoolExecutor为主线解释它怎样接收任务、怎样选参数以及在 Android 中怎样处理 UI 回调、生命周期和后台工作。一、先弄清任务、线程和线程池任务是待执行的工作通常表示为Runnable或CallableT。线程是实际执行任务的资源。新建线程有成本也占用栈内存。线程池维护工作线程并管理尚未执行的任务。一个线程池通常由四部分组成工作线程、任务队列、线程工厂和拒绝策略。应用提交任务后线程池决定立即运行、排队等待还是拒绝。调用方提交任务 ↓ 核心线程未满 ── 是 → 创建工作线程并执行 │否 ↓ 队列有空位 ─── 是 → 入队等待 │否 ↓ 线程数未达到上限 是 → 创建非核心线程并执行 │否 ↓ 执行拒绝策略这里有一个反直觉之处达到核心线程数后线程池通常先排队队列满了才增加到最大线程数。如果队列永远装不满maximumPoolSize基本不会发挥作用。二、ThreadPoolExecutor的七个关键参数valexecutorThreadPoolExecutor(2,// corePoolSize4,// maximumPoolSize30L,// keepAliveTimeTimeUnit.SECONDS,// unitArrayBlockingQueueRunnable(32),// workQueuenamedThreadFactory,// threadFactoryThreadPoolExecutor.AbortPolicy()// handler)参数作用常见误解corePoolSize常驻工作线程的目标数量初始就一定创建这么多线程默认是按需创建maximumPoolSize线程总数上限提交任务超过核心数后立即扩容通常先入队keepAliveTime空闲的非核心线程存活时间默认也会回收核心线程需显式调用allowCoreThreadTimeOut(true)workQueue等待执行的任务队列队列越大越安全大队列可能积压过期任务threadFactory创建和命名工作线程只是装饰线程名能显著帮助排查 ANR 和崩溃handler队列与线程均满时的处理方式拒绝只会在关机时发生容量耗尽也会触发用数字走一遍执行过程假设核心线程数为 2、最大线程数为 4、队列容量为 4且前面的任务都没有完成提交顺序结果第 12 个任务创建核心线程并执行第 36 个任务进入队列第 78 个任务创建非核心线程并执行第 9 个及以后执行拒绝策略实际运行时已有任务可能在下一次提交前完成因此表格描述的是“所有任务持续占用线程”的瞬间状态。常见队列该怎么选ArrayBlockingQueue(capacity)固定容量适合给图片处理、文件解析等任务设置明确的积压上限。LinkedBlockingQueue(capacity)也可设置容量务必显式传入上限。SynchronousQueue不存储任务提交时直接移交给工作线程常与可扩容线程池配合需要严格控制最大线程数。PriorityBlockingQueue按优先级取任务默认无界高优先级任务持续进入时低优先级任务可能长期等待。对移动端页面而言有界队列通常更容易控制峰值内存与过期任务数量。三、一个适合 Android 图片处理的线程池下面的例子为图片解码建立一个应用级线程池。它限制并发数、限制排队量并为工作线程命名classDecodeExecutors{privatevalnextIdAtomicInteger(0)valpool:ExecutorServiceThreadPoolExecutor(2,4,30L,TimeUnit.SECONDS,ArrayBlockingQueueRunnable(32),ThreadFactory{task-Thread(task,thumbnail-decode-${nextId.incrementAndGet()})},ThreadPoolExecutor.AbortPolicy())/** 仅在确实结束该线程池的所有权时调用例如测试清理。 */funshutdown()pool.shutdown()}把DecodeExecutors作为应用级依赖注入单例即可。Activity 或 ViewModel 只借用线程池不在自身销毁时关闭共享线程池它们应该取消自己提交的任务。关闭共享池会使其他页面后续提交任务时收到RejectedExecutionException。为什么不直接使用Executors.newFixedThreadPool()newFixedThreadPool(n)使用无界的LinkedBlockingQueue。线程数虽然固定等待任务却可能不断累积。用户快速滑动长列表时旧图片请求排在队列里不但占内存还可能在页面已经不需要它们时继续执行。newSingleThreadExecutor()也有类似的无界排队风险newCachedThreadPool()则可能在任务大量到来时创建过多线程。它们并非不能用但要先确认任务来源和提交速率确实受控。四、在 ViewModel 中提交任务并安全回到主线程Android 的 View 只能在主线程更新。线程池里的解码任务结束后需要切回主线程同时防止页面已销毁或新请求已覆盖旧请求。funinterfaceThumbnailDecoder{fundecode(uri:Uri):Bitmap}sealedinterfaceThumbnailState{objectLoading:ThumbnailStatedataclassReady(valbitmap:Bitmap):ThumbnailStatedataclassError(valcause:Exception):ThumbnailState}classThumbnailViewModel(privatevaldecoder:ThumbnailDecoder,privatevalexecutor:ExecutorService):ViewModel(){privatevalmainHandler(Looper.getMainLooper())privatevalgenerationAtomicInteger(0)privatevarcurrentTask:Future*?nullprivateval_stateMutableLiveDataThumbnailState()valstate:LiveDataThumbnailState_state/** 由主线程调用。 */funload(uri:Uri){valrequestIdgeneration.incrementAndGet()currentTask?.cancel(true)_state.valueThumbnailState.Loadingtry{currentTaskexecutor.submit{valresulttry{ThumbnailState.Ready(decoder.decode(uri))}catch(e:InterruptedException){Thread.currentThread().interrupt()returnsubmit}catch(e:Exception){ThumbnailState.Error(e)}if(Thread.currentThread().isInterrupted)returnsubmitmain.post{if(requestIdgeneration.get()){_state.valueresult}}}}catch(e:RejectedExecutionException){_state.valueThumbnailState.Error(e)}}overridefunonCleared(){generation.incrementAndGet()currentTask?.cancel(true)super.onCleared()}}这段代码有四个关键点submit()返回Future页面不再需要结果时可以调用cancel(true)。Handler(Looper.getMainLooper())将状态更新切回主线程。generation阻止旧请求覆盖新请求onCleared()后排队的回调也会失效。提交端处理RejectedExecutionException避免队列满时直接崩溃。cancel(true)只是发出中断请求无法保证底层解码或网络 I/O 立即停止。耗时循环应定期检查中断状态对不响应中断的 SDK还需要使用它自身的取消 API。代码中的ThumbnailDecoder是示例接口实际实现应关闭输入流并按目标尺寸采样解码避免加载原图导致内存峰值过高。如果页面已经使用 Kotlin 协程通常可以通过viewModelScope.launch和withContext(Dispatchers.IO)完成生命周期感知的异步工作。只有需要独立限制某类任务并发数、队列长度或拒绝行为时才需要专门持有线程池。协程是任务组织方式线程池负责执行资源两者可以配合使用。五、execute()、submit()和异常处理executor.execute{// 只关心执行不需要返回 Future}valfuture:FutureIntexecutor.submit(Callable{computeScore()})方法返回值异常在哪里出现execute(Runnable)无未捕获异常会到工作线程的未捕获异常处理流程submit(Runnable/Callable)Future任务异常保存在Future调用get()时包装为ExecutionException不要在 Android 主线程调用耗时任务的future.get()它会阻塞主线程可能造成掉帧或 ANR。需要结果时使用回调、主线程Handler、协程或响应式数据流。使用submit()后如果既不读取Future也不在任务内部记录错误失败可能变得难以发现。建议在任务边界记录异常并将可恢复错误转换为明确的 UI 状态。六、拒绝策略就是“背压”决策当线程数和队列都达到上限继续接收任务只会增加延迟或内存压力。常见策略如下策略行为Android 中的注意点AbortPolicy抛出RejectedExecutionException最明确调用方必须处理失败或稍后重试CallerRunsPolicy由提交任务的线程执行若从主线程提交重任务会直接卡住 UIDiscardPolicy静默丢弃新任务可能让页面一直等待结果不建议用于必须完成的任务DiscardOldestPolicy丢弃队头任务并重试被丢任务的调用方未必知道需要非常清楚业务语义图片缩略图、搜索建议这类“新请求可替代旧请求”的工作可以在业务层取消过期任务或合并相同请求。订单提交、支付确认等任务不能仅靠线程池排队保障可靠性需要明确的幂等、持久化和失败恢复方案。七、线程数应该设多少没有适用于所有 Android 设备的固定值。先看任务究竟在等待什么CPU 密集型图片变换、压缩、加密、复杂计算。并发数通常从设备可用处理器数量附近开始试验同时考虑 UI、渲染和其他应用也需要 CPU。I/O 密集型磁盘读写或网络等待。可以允许多于 CPU 核数的任务并发但仍需受文件句柄、服务端限流、连接池、内存和电量约束。混合型把解码、磁盘和网络分别测量不要用“一个万能线程池”承载所有任务。常见的估算式线程数 ≈ CPU 核数 × (1 等待时间/计算时间)只能作为起点。移动设备的性能核、能效核、温控和前台交互都在变化最终应通过真实机型上的延迟、CPU、内存与电量数据调整。队列容量同样需要根据峰值提交速率与可接受等待时间确定。如果 1 秒内可能提交 100 张缩略图而用户只看得到屏幕附近的十几张图片扩大队列通常只会让过期工作排得更长。优先限制生产速率、取消离屏任务并使用图片库提供的缓存与去重能力。八、关闭、取消与生命周期三个不同动作task.cancel(true)// 取消一个任务运行中的任务收到中断请求executor.shutdown()// 不再接收新任务已提交任务继续执行executor.shutdownNow()// 尝试中断运行中任务并返回尚未开始的任务shutdownNow()也无法强制停止不响应中断的代码。需要等待线程池终止时awaitTermination()应放在非主线程运行。在 Android 中按“谁拥有线程池、谁拥有任务”来管理生命周期Activity/Fragment/ViewModel 持有自己提交任务的Future在不再需要结果时取消。应用级单例拥有共享线程池页面不能随意关闭它。某个临时功能独占的线程池由该功能在真正结束时关闭。页面旋转或进程死亡后仍必须完成的工作不应只依赖进程内线程池。最后一点尤其重要线程池里的排队任务只存在于当前进程。系统回收进程后它们不会自动恢复。九、线程池、协程、WorkManager 怎么选场景首选工具原因页面可见期间加载数据viewModelScope 协程生命周期取消、顺序表达与主线程切换更直接对某类短任务设明确并发/队列上限专用ThreadPoolExecutor可控制线程数、队列、命名与拒绝行为必须在进程结束后仍可调度的可延迟工作WorkManager持久化调度、约束条件和重试机制长时间、用户可感知且需持续运行的任务按平台规则设计前台服务等机制需要符合 Android 后台执行限制Retrofit/OkHttp、成熟图片库已有异步能力优先使用库提供的机制避免重复排队和额外线程层级WorkManager 适合可以延迟执行的可靠后台工作但并不意味着任务一定会立刻运行。用户正在等待的页面请求仍应使用前台生命周期内的异步方式。线程池也不提供 WorkManager 的持久化与系统调度能力。十、排查线程池问题时看什么ThreadPoolExecutor提供一些运行指标valpoolexecutorasThreadPoolExecutor Log.d(DecodePool,active${pool.activeCount}, size${pool.poolSize}, queued${pool.queue.size}, completed${pool.completedTaskCount})这些是采样值不保证在读取后仍不变。排查时重点看队列长度是否持续增长任务是否已经过期拒绝次数是否增加任务从提交到开始执行的等待时间真正执行耗时与主线程掉帧是否同时升高是否有任务被取消后仍长期占用线程。如果队列持续增长先检查任务生产速度和取消策略再考虑增加线程数。线程数翻倍可能只是让 CPU、磁盘或服务端更早达到瓶颈。总结线程池使用的核心不是记住某个“最佳线程数”而是回答四个问题最多允许多少任务同时执行最多允许多少任务等待满了以后怎么办页面或进程结束时任务如何处理在 Android 中一个配置清楚的有界线程池能帮助我们限制图片、解析等短任务的资源占用协程让页面级异步流程更易管理WorkManager 则处理需要系统持久化调度的后台任务。根据任务的生命周期和可靠性要求选择工具比单纯把代码扔进后台线程更重要。参考资料JavaThreadPoolExecutorAPI 文档JavaExecutorsAPI 文档Android 后台任务指南Android 协程指南Android WorkManager 指南