Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

多线程并发专题

Android 并发题要同时答 Java 基础和移动端场景:线程池参数背得出只是起点,还要知道 HandlerThread、IntentService、协程调度、锁、CAS、ANR/OOM 怎么落地排查。

一、Android 线程模型与 HandlerThread

主线程负责 UI、输入事件和生命周期回调,耗时任务必须离开主线程。HandlerThread 是带 Looper 的后台线程,适合串行处理一类任务。

val thread = HandlerThread("worker").apply { start() }
val handler = Handler(thread.looper)
handler.post { /* 串行后台任务 */ }

特点:

  • 一个 HandlerThread 对应一个 Looper/MessageQueue,任务按消息顺序串行执行。
  • 适合相机回调、轻量 IO、SDK 内部串行状态机。
  • 不适合大量并行任务;任务太慢会阻塞后续消息。
  • 退出时调用 quitSafely(),避免线程泄漏。

二、IntentService / JobIntentService 的边界

IntentService 本质是 Service + HandlerThread,按 Intent 串行执行,执行完自动停止。它曾适合后台串行任务,但在后台执行限制增强后使用场景变窄。

机制特点现状/边界
IntentService后台线程串行处理 Intent已不推荐作为新方案,受后台限制影响
JobIntentService兼容低版本,高版本走 JobScheduler 思路也不是长期首选,复杂任务更推荐 WorkManager
WorkManager可约束、可重试、可持久化延迟/保证执行类后台任务首选

怎么答:如果是页面内短任务,用协程/线程池;如果是可延迟、需约束、进程死后仍要执行的任务,用 WorkManager,不要滥用 Service 常驻后台。

三、Executor 与 ThreadPoolExecutor

线程池用于复用线程、限制并发、隔离任务类型。ThreadPoolExecutor 关键参数不是背诵,要能讲执行流程:

提交任务
  -> 工作线程数 < corePoolSize: 新建核心线程
  -> 否则尝试入队 workQueue
  -> 队列满且线程数 < maximumPoolSize: 新建非核心线程
  -> 仍无法处理: RejectedExecutionHandler
参数含义Android 坑点
corePoolSize核心线程数太大增加调度和内存压力
maximumPoolSize最大线程数配无界队列时通常不起作用
workQueue等待队列无界队列可能堆积导致 OOM
keepAliveTime非核心线程存活时间可回收突发线程
rejectedHandler拒绝策略要可观测,不要静默丢任务

四、协程 Dispatchers 与线程池关系

Kotlin 协程不是“没有线程”,它是挂起/恢复的调度模型,最终仍运行在线程上。

  • Dispatchers.Main:主线程,更新 UI。
  • Dispatchers.IO:IO 密集型任务,适合网络、磁盘,底层有弹性线程池策略。
  • Dispatchers.Default:CPU 密集型任务,如排序、JSON 大计算、图片算法。
  • withContext:切换执行上下文并等待结果。

怎么落地:Repository 做网络/数据库可用 withContext(IO);CPU 计算不要丢到 IO;UI 收集 Flow 用生命周期感知 API,避免页面销毁后继续更新。

五、锁、volatile、CAS 与 Atomic

并发控制要区分“可见性、原子性、有序性”。

工具解决什么适合场景注意点
synchronized互斥 + 可见性简单临界区持锁不要做 Binder/IO
ReentrantLock可中断/可尝试/公平锁复杂锁控制必须 finally unlock
volatile可见性 + 禁止部分重排状态标记、双检锁引用不能保证复合操作原子性
CAS/Atomic无锁原子更新计数、状态机引用ABA、自旋开销
private val running = AtomicBoolean(false)

fun startOnce() {
    if (running.compareAndSet(false, true)) {
        // only one caller can enter
    }
}

六、死锁与线程安全排查

死锁通常满足互斥、持有并等待、不可抢占、循环等待。Android 里高频场景是“主线程等后台锁,后台线程切回主线程”或“持锁发同步 Binder”。

排查路径:

  1. 抓 ANR traces 或线程 dump,看主线程卡在哪把锁/哪个 Future/Binder。
  2. 找持锁线程,看它是否等待主线程、IO、网络或另一个锁。
  3. 检查锁顺序是否全局一致,是否在锁内做耗时操作。
  4. 用超时、tryLock、缩小临界区或串行队列替代复杂嵌套锁。

怎么答:不要只说“加锁解决线程安全”,还要补“锁粒度、锁顺序、锁内不做耗时/跨进程调用”。

七、线程池导致 ANR/OOM 的坑

线程池配置错会从“优化”变成“事故”。

  • ANR:主线程等待线程池结果,但线程池被长任务占满;或回调切主线程后主线程被阻塞。
  • OOM:无界队列堆积大量 Runnable/闭包持有 Activity/Bitmap;线程数过多导致栈内存暴涨。
  • 优先级反转:低优先级长任务占满池,高优先级 UI 相关任务排队。
  • 任务泄漏:页面销毁后任务仍持有 View/Context。
规避方式
newCachedThreadPool 滥用限制最大线程数,按任务类型隔离线程池
无界 LinkedBlockingQueue设置有界队列和拒绝策略
主线程 Future.get()用回调/协程挂起,不要阻塞主线程
IO/CPU 混用一个池IO 与 CPU 任务隔离,避免互相饿死

八、并发设计落地模板

面试讲项目时可以按这个模板说明并发方案:

  1. UI 事件进入 ViewModel,用协程保证生命周期自动取消。
  2. Repository IO 任务切到 Dispatchers.IO,CPU 计算切到 Default
  3. SDK 内部串行状态用 HandlerThread 或单线程 Executor。
  4. 共享状态用不可变快照、Atomic 或小粒度锁。
  5. 线程池设置有界队列、命名线程、异常日志和拒绝策略。

高频面试题

Q1:HandlerThread 适合什么场景?和线程池区别? HandlerThread 是一个带 Looper 的单后台线程,任务按消息串行执行,适合相机、SDK 状态机、轻量 IO 等需要顺序的任务。线程池适合多个独立任务并发执行,但要控制队列和线程数。

Q2:ThreadPoolExecutor 的执行流程是什么? 先看工作线程是否小于 corePoolSize,是则建核心线程;否则入队;队列满且线程数小于 maximumPoolSize 时建非核心线程;仍处理不了就走拒绝策略。无界队列会让 maximumPoolSize 基本失效。

Q3:volatile 能保证 i++ 线程安全吗? 不能。volatile 保证可见性和一定有序性,但 i++ 是读-改-写复合操作,不具备原子性。需要 synchronized、Lock 或 AtomicInteger/CAS。

Q4:协程 Dispatchers.IO 和 Default 怎么选? IO 用于网络、磁盘、数据库等阻塞 IO;Default 用于 CPU 密集计算。协程最终仍在线程上执行,选错调度器会导致线程饥饿或 CPU 争用。

Q5:线程池为什么会导致 ANR 或 OOM? 主线程等待线程池结果会 ANR;线程池被长任务占满会让关键任务排队。无界队列堆积大量 Runnable、线程数过多导致栈内存增长、任务闭包持有大对象,都可能 OOM。

易错点 / 追问

  • 协程不是替代线程的魔法,挂起恢复最终仍依赖 Dispatcher 背后的线程。
  • volatile 不能保证复合操作原子性,回答时要区分可见性和原子性。
  • 锁内不要做 IO、网络、同步 Binder 或切主线程等待,这是死锁/ANR 高频点。
  • 无界队列 + 大量任务比“线程数很多”更隐蔽,也更容易拖到 OOM。
  • IntentService/JobIntentService 不是现代后台任务万能解,可延迟可靠任务优先考虑 WorkManager。