4. 并发编程与线程安全:锁机制、Handler消息机制、协程
并发编程是 Android 架构面试的核心难点,涉及多线程数据竞争、主线程消息处理以及 Kotlin 协程的非阻塞异步。面试官常从原理理解、性能对比、实际场景选择三个维度考察。掌握以下回答框架和对比表格,可快速构建结构化表达。
三种并发机制核心对比
| 对比维度 | 锁机制 (synchronized/ReentrantLock) | Handler + Looper | Kotlin 协程 |
|---|---|---|---|
| 基本原理 | 基于操作系统互斥量或 CAS,阻塞等待 | 消息队列 + 事件循环,非阻塞投递 | 挂起函数 + Continuation,非阻塞切换 |
| 适用场景 | 保护共享变量、临界区 | 线程间通信、UI 更新 | 异步任务、结构化并发、网络请求 |
| 性能特点 | 高竞争时开销大,可能死锁 | 轻量级消息传递,无锁竞争 | 极低挂起开销,支持百万级协程 |
| 常见陷阱 | 锁粒度太大、忘记解锁 | 未调用 Looper.prepare()、内存泄漏 | 协程逃逸、异常未捕获、Scope 管理 |
📌 问答卡片 1:锁机制
面试问题:synchronized 和 ReentrantLock 有何区别?你如何选择?
面试官考察点:锁底层原理、公平性、可中断性、性能取舍。
回答框架:
- 对比基础:synchronized 是关键字,自动释放;ReentrantLock 是 API,需手动 unlock。
- 特性差异:公平锁、可中断、条件变量、尝试获取锁(tryLock)。
- 性能:JDK6 后 synchronized 经锁优化(偏向锁、轻量锁)性能接近,但高竞争时 ReentrantLock 更稳定。
- 选择原则:简单场景用 synchronized,需要超时、中断或多个条件时用 ReentrantLock。
示例回答:synchronized 是 JVM 内置锁,自动管理,适合大多数简单同步;ReentrantLock 更灵活,支持公平性、可中断、tryLock 避免死锁。性能方面,现代 JVM 两者差异不大,我优先使用 synchronized,只有在需要超时控制或公平排队时才改用 ReentrantLock。
可能追问:如何避免死锁?锁的降级和升级?
避坑提醒:不要使用 synchronized 修饰大量代码块,保持锁粒度小;使用 ReentrantLock 务必在 finally 中 unlock。
📌 问答卡片 2:Handler 消息机制
面试问题:Handler 是如何实现线程切换的?Looper、MessageQueue、Handler 三者的关系是什么?
面试官考察点:消息循环模型、线程绑定、内存泄漏风险。
回答框架:
- 线程绑定:每个线程可包含一个 Looper(ThreadLocal),Looper 持有 MessageQueue。
- 消息流转:Handler.sendMessage() 将消息加入目标线程的 MessageQueue,Looper.loop() 不断取出消息并回调 handleMessage。
- 切换本质:因为 Handler 关联了目标线程的 Looper,所以消息处理在目标线程执行。
- 注意事项:非主线程需手动调用 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 传递状态机)、非阻塞挂起、调度器作用。
回答框架:
- 本质:协程是用户态轻量级线程,通过挂起代替阻塞,不消耗 OS 线程。
- 挂起实现:每个挂起函数会编译为 Continuation 对象,保存局部变量和恢复点,挂起时返回,由调度器决定何时恢复。
- 线程区别:协程运行在线程上,但挂起时释放线程资源,可让其它协程使用该线程。
- 调度器:Dispatchers.Default/IO/Main 控制协程在哪个线程执行。
示例回答:协程通过编译器将挂起函数转换为状态机,挂起点保存恢复信息(Continuation),挂起后线程可执行其他任务,实现非阻塞。与线程相比,协程占用栈空间极小(KB级),可轻松创建百万级,并通过调度器灵活分配线程,而线程切换有内核态开销。
可能追问:协程异常处理(SupervisorJob vs Job)?withContext 与 launch 的区别?
避坑提醒:协程体中的 try-catch 不能包裹 launch,需使用 SupervisorJob 或 CoroutineExceptionHandler;注意取消协程后的资源共享问题。
💡 面试加分点:在回答并发相关问题时,主动对比三种机制的本质差异,并举例说明实际项目中如何权衡(如:网络请求用协程、UI 更新用 Handler、共享数据用锁)。能结合 Android 源码走向(如 Handler 的 IdleHandler、协程的挂起性能数据)会给面试官留下深刻印象。