4. 并发编程与线程安全:锁机制、Handler消息机制、协程

并发编程是 Android 架构面试的核心难点,涉及多线程数据竞争、主线程消息处理以及 Kotlin 协程的非阻塞异步。面试官常从原理理解、性能对比、实际场景选择三个维度考察。掌握以下回答框架和对比表格,可快速构建结构化表达。

识别问题类型 阐述核心原理 对比性能差异 给出最佳实践 提示常见陷阱 同步/锁 vs Handler vs 协程 底层机制与线程模型 吞吐量、锁粒度、内存开销 按场景选择,避免过度设计 死锁、内存泄漏、协程取消异常

三种并发机制核心对比

对比维度 锁机制 (synchronized/ReentrantLock) Handler + Looper Kotlin 协程
基本原理 基于操作系统互斥量或 CAS,阻塞等待 消息队列 + 事件循环,非阻塞投递 挂起函数 + Continuation,非阻塞切换
适用场景 保护共享变量、临界区 线程间通信、UI 更新 异步任务、结构化并发、网络请求
性能特点 高竞争时开销大,可能死锁 轻量级消息传递,无锁竞争 极低挂起开销,支持百万级协程
常见陷阱 锁粒度太大、忘记解锁 未调用 Looper.prepare()、内存泄漏 协程逃逸、异常未捕获、Scope 管理

📌 问答卡片 1:锁机制

面试问题:synchronized 和 ReentrantLock 有何区别?你如何选择?

面试官考察点:锁底层原理、公平性、可中断性、性能取舍。

回答框架:

  1. 对比基础:synchronized 是关键字,自动释放;ReentrantLock 是 API,需手动 unlock。
  2. 特性差异:公平锁、可中断、条件变量、尝试获取锁(tryLock)。
  3. 性能:JDK6 后 synchronized 经锁优化(偏向锁、轻量锁)性能接近,但高竞争时 ReentrantLock 更稳定。
  4. 选择原则:简单场景用 synchronized,需要超时、中断或多个条件时用 ReentrantLock。

示例回答:synchronized 是 JVM 内置锁,自动管理,适合大多数简单同步;ReentrantLock 更灵活,支持公平性、可中断、tryLock 避免死锁。性能方面,现代 JVM 两者差异不大,我优先使用 synchronized,只有在需要超时控制或公平排队时才改用 ReentrantLock。

可能追问:如何避免死锁?锁的降级和升级?

避坑提醒:不要使用 synchronized 修饰大量代码块,保持锁粒度小;使用 ReentrantLock 务必在 finally 中 unlock。

📌 问答卡片 2:Handler 消息机制

面试问题:Handler 是如何实现线程切换的?Looper、MessageQueue、Handler 三者的关系是什么?

面试官考察点:消息循环模型、线程绑定、内存泄漏风险。

回答框架:

  1. 线程绑定:每个线程可包含一个 Looper(ThreadLocal),Looper 持有 MessageQueue。
  2. 消息流转:Handler.sendMessage() 将消息加入目标线程的 MessageQueue,Looper.loop() 不断取出消息并回调 handleMessage。
  3. 切换本质:因为 Handler 关联了目标线程的 Looper,所以消息处理在目标线程执行。
  4. 注意事项:非主线程需手动调用 Looper.prepare() 和 Looper.loop()。

示例回答:Handler 通过构造函数绑定目标线程的 Looper(主线程 Looper 在 ActivityThread 中已准备)。当调用 sendMessage 时,消息入队到该 Looper 的 MessageQueue;Looper.loop() 在目标线程不断取出消息并调用 handler.handleMessage,从而完成线程切换。非主线程需先 Looper.prepare() 创建 Looper。

可能追问:主线程 Looper 何时启动?Handler 的 dispatchMessage 流程?

避坑提醒:内部类 Handler 持有 Activity 引用导致内存泄漏,应使用静态内部类 + WeakReference。

📌 问答卡片 3:Kotlin 协程

面试问题:挂起函数的工作原理是什么?协程与线程的本质区别?

面试官考察点:协程底层(Continuation 传递状态机)、非阻塞挂起、调度器作用。

回答框架:

  1. 本质:协程是用户态轻量级线程,通过挂起代替阻塞,不消耗 OS 线程。
  2. 挂起实现:每个挂起函数会编译为 Continuation 对象,保存局部变量和恢复点,挂起时返回,由调度器决定何时恢复。
  3. 线程区别:协程运行在线程上,但挂起时释放线程资源,可让其它协程使用该线程。
  4. 调度器:Dispatchers.Default/IO/Main 控制协程在哪个线程执行。

示例回答:协程通过编译器将挂起函数转换为状态机,挂起点保存恢复信息(Continuation),挂起后线程可执行其他任务,实现非阻塞。与线程相比,协程占用栈空间极小(KB级),可轻松创建百万级,并通过调度器灵活分配线程,而线程切换有内核态开销。

可能追问:协程异常处理(SupervisorJob vs Job)?withContext 与 launch 的区别?

避坑提醒:协程体中的 try-catch 不能包裹 launch,需使用 SupervisorJob 或 CoroutineExceptionHandler;注意取消协程后的资源共享问题。

💡 面试加分点:在回答并发相关问题时,主动对比三种机制的本质差异,并举例说明实际项目中如何权衡(如:网络请求用协程、UI 更新用 Handler、共享数据用锁)。能结合 Android 源码走向(如 Handler 的 IdleHandler、协程的挂起性能数据)会给面试官留下深刻印象。