Messenger 是否支持并发调用?如何保证线程安全?
解读
国内面试中,这道题表面问“并发”,实则考察你对 Android 跨进程通信(IPC)机制、HandlerLooper 模型、Binder 线程池以及线程安全设计范式的综合掌握。面试官希望你:
- 先给出“是否并发”的明确结论;
- 用源码级论据说明“为什么”;
- 结合真实业务场景给出“怎么做”——既不让主线程卡死,也不让并发数据竞争崩溃;
- 最后能升华到“如果不用 Messenger,还有哪些更合适的并发 IPC 方案”。
知识点
- Messenger 本质:对 Binder 的轻量级封装,底层依赖 Handler/Looper 队列。
- Binder 线程池:Server 端默认线程池上限 16(
binder.c的MAX_BINDER_THREADS),每个调用会随机分配到线程池线程,天然具备“多线程并发”特征。 - Handler 队列模型:单线程串行执行,但“单线程”指的是每个 Messenger 背后绑定的 Looper 线程;如果客户端多个线程同时
send(Message),这些 Message 会并发进入 Binder 驱动,Server 端线程池会并发回调handleMessage(Message)。 - 线程安全关键点:Handler 的
handleMessage回调在 Server 端是“多线程并发”执行的,因此共享状态必须做同步;而客户端send()本身无锁,但 Message 对象一旦复用就要注意可见性与逃逸。 - 国内常见坑:在主线程创建 Messenger 并直接处理耗时逻辑,导致 ANR;把
Message.obj传可变对象,多线程修改后值错乱;使用Message.obtain()复用池时,把 Message 当成员变量缓存,导致并发回收冲突。 - 替代方案:AIDL(自定义线程策略)、ContentProvider(apply-batch 异步)、共享内存(Ashmem/MM)+ 文件锁、Socket/Unix Domain Socket、Kotlin 协程通道 + 共享内存等。
答案
结论:Messenger 在底层 Binder 线程池层面“支持并发调用”,但 Server 端的 handleMessage 回调是“多线程并发”执行的,因此业务代码必须自己保证线程安全。
具体论证:
- 客户端任意线程可并发调用
mMessenger.send(msg),Binder 驱动会把请求分发给 Server 端线程池中的不同线程,故“并发调用”客观存在。 - Server 端如果通过
new Messenger(new Handler(Looper.getMainLooper()))创建,则所有回调被主线程 Looper 串行化,天然线程安全,但容易 ANR;如果直接在Service.onCreate()里new Messenger(new Handler(Looper.myLooper()))且该 Service 运行在非主线程,则handleMessage会在该 Looper 线程串行执行;若使用默认线程池(未指定 Looper),则每次回调都在 Binder 线程池的随机线程,此时共享变量必须加锁或使用原子类。 - 官方推荐做法:
- 若业务简单、无状态,直接把 Messenger 绑定到主线程 Handler,保证串行;
- 若业务重、IO 多,在独立 HandlerThread 中创建 Looper,再 new Messenger,既避免 ANR,又利用 Looper 队列串行化,消除锁开销;
- 若必须利用多核并发,则放弃 Messenger,改用 AIDL +
CopyOnWriteArrayList/ConcurrentHashMap自行管理线程安全,或使用BinderThreadPoolExecutor控制并发度。
- 代码模板(国内面试可直接手写):
class ConcurrentService : Service() { private val handlerThread = HandlerThread("MessengerBg").apply { start() } private val messenger = Messenger(Handler(handlerThread.looper) { msg -> // 此回调在 handlerThread 单线程串行执行,无需额外锁 when (msg.what) { MSG_CALC -> { /* 耗时计算 */ } } true }) override fun onBind(intent: Intent) = messenger.binder } - 线程安全 checklist:
- Message 不复用出栈后再修改;
- 不直接把
List/Map当msg.obj传递,改用Bundle或Parcelable快照; - Server 端若需访问公共缓存,使用
ConcurrentHashMap或synchronized块; - 对于“请求-响应”式并发,采用
AtomicInteger生成唯一msg.replyTo密钥,防止回调错配。
拓展思考
- 在国内厂商定制 ROM(如 MIUI、EMUI)中,Binder 线程池上限可能被改到 32,但并发越高越易触发
TransactionTooLargeException,需要把大数据拆块或改用共享内存。 - 如果业务需要“流式高并发”IPC(如车载 IVI 系统 60 fps 传图),Messenger 的 Handler 队列会成为瓶颈,可改用:
AIDL + SharedMemory + Fd零拷贝;Unix Domain Socket + epoll边缘触发;- Kotlin 协程
Channel+MemoryFile双缓冲,实现背压与挂起。
- 对于“跨进程事件总线”场景,可封装 Messenger 为
Flow或RxJava流,利用callbackFlow把handleMessage发送到协程通道,下游用buffer(Channel.UNLIMITED)收集,既保留 Messenger 的轻量,又享受协程结构化并发与Dispatchers.Default自动线程切换。