Messenger 是否支持并发调用?如何保证线程安全?

解读

国内面试中,这道题表面问“并发”,实则考察你对 Android 跨进程通信(IPC)机制、HandlerLooper 模型、Binder 线程池以及线程安全设计范式的综合掌握。面试官希望你:

  1. 先给出“是否并发”的明确结论;
  2. 用源码级论据说明“为什么”;
  3. 结合真实业务场景给出“怎么做”——既不让主线程卡死,也不让并发数据竞争崩溃;
  4. 最后能升华到“如果不用 Messenger,还有哪些更合适的并发 IPC 方案”。

知识点

  1. Messenger 本质:对 Binder 的轻量级封装,底层依赖 Handler/Looper 队列。
  2. Binder 线程池:Server 端默认线程池上限 16(binder.cMAX_BINDER_THREADS),每个调用会随机分配到线程池线程,天然具备“多线程并发”特征。
  3. Handler 队列模型:单线程串行执行,但“单线程”指的是每个 Messenger 背后绑定的 Looper 线程;如果客户端多个线程同时 send(Message),这些 Message 会并发进入 Binder 驱动,Server 端线程池会并发回调 handleMessage(Message)
  4. 线程安全关键点:Handler 的 handleMessage 回调在 Server 端是“多线程并发”执行的,因此共享状态必须做同步;而客户端 send() 本身无锁,但 Message 对象一旦复用就要注意可见性与逃逸。
  5. 国内常见坑:在主线程创建 Messenger 并直接处理耗时逻辑,导致 ANR;把 Message.obj 传可变对象,多线程修改后值错乱;使用 Message.obtain() 复用池时,把 Message 当成员变量缓存,导致并发回收冲突。
  6. 替代方案:AIDL(自定义线程策略)、ContentProvider(apply-batch 异步)、共享内存(Ashmem/MM)+ 文件锁、Socket/Unix Domain Socket、Kotlin 协程通道 + 共享内存等。

答案

结论:Messenger 在底层 Binder 线程池层面“支持并发调用”,但 Server 端的 handleMessage 回调是“多线程并发”执行的,因此业务代码必须自己保证线程安全

具体论证:

  1. 客户端任意线程可并发调用 mMessenger.send(msg),Binder 驱动会把请求分发给 Server 端线程池中的不同线程,故“并发调用”客观存在。
  2. Server 端如果通过 new Messenger(new Handler(Looper.getMainLooper())) 创建,则所有回调被主线程 Looper 串行化,天然线程安全,但容易 ANR;如果直接在 Service.onCreate()new Messenger(new Handler(Looper.myLooper())) 且该 Service 运行在非主线程,则 handleMessage 会在该 Looper 线程串行执行;若使用默认线程池(未指定 Looper),则每次回调都在 Binder 线程池的随机线程,此时共享变量必须加锁或使用原子类。
  3. 官方推荐做法:
    • 若业务简单、无状态,直接把 Messenger 绑定到主线程 Handler,保证串行;
    • 若业务重、IO 多,在独立 HandlerThread 中创建 Looper,再 new Messenger,既避免 ANR,又利用 Looper 队列串行化,消除锁开销;
    • 若必须利用多核并发,则放弃 Messenger,改用 AIDL + CopyOnWriteArrayList / ConcurrentHashMap 自行管理线程安全,或使用 BinderThreadPoolExecutor 控制并发度。
  4. 代码模板(国内面试可直接手写):
    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
    }
    
  5. 线程安全 checklist:
    • Message 不复用出栈后再修改;
    • 不直接把 List/Mapmsg.obj 传递,改用 BundleParcelable 快照;
    • Server 端若需访问公共缓存,使用 ConcurrentHashMapsynchronized 块;
    • 对于“请求-响应”式并发,采用 AtomicInteger 生成唯一 msg.replyTo 密钥,防止回调错配。

拓展思考

  1. 在国内厂商定制 ROM(如 MIUI、EMUI)中,Binder 线程池上限可能被改到 32,但并发越高越易触发 TransactionTooLargeException,需要把大数据拆块或改用共享内存。
  2. 如果业务需要“流式高并发”IPC(如车载 IVI 系统 60 fps 传图),Messenger 的 Handler 队列会成为瓶颈,可改用:
    • AIDL + SharedMemory + Fd 零拷贝;
    • Unix Domain Socket + epoll 边缘触发;
    • Kotlin 协程 Channel + MemoryFile 双缓冲,实现背压与挂起。
  3. 对于“跨进程事件总线”场景,可封装 Messenger 为 FlowRxJava 流,利用 callbackFlowhandleMessage 发送到协程通道,下游用 buffer(Channel.UNLIMITED) 收集,既保留 Messenger 的轻量,又享受协程结构化并发与 Dispatchers.Default 自动线程切换。