文章内容
本文内容基于艾文老师(小米)的《06-MessageQueue如何保持各线程添加消息的线程安全?》讲解视频整理
多个线程可以同时向一个 Handler 发消息,也可以通过不同 Handler 向同一个 Looper 投递任务。发送方各自运行,最终却要修改同一个 MessageQueue:谁负责防止消息丢失、链表断开或队列状态混乱?
答案是队列。在本文分析的实现中,MessageQueue 使用自身对象的监视器锁,把消息链表的插入、摘除和清理放进同一套互斥规则中。 发送方可以有多个线程,消费者通常只有 Looper 所在线程,但双方都需要遵守这套规则。
一、跨线程消息投递与消费
Handler、Looper 和 MessageQueue 的分工可以先用一条调用链串起来:
发送方线程 Handler.sendMessage / post → Handler 设置 Message.target 等字段 → MessageQueue.enqueueMessage Looper 所在线程 MessageQueue.next → Handler.dispatchMessage → Runnable / Handler.Callback / handleMessage调用 sendMessage() 时,入队代码仍在发送方线程上执行。只有消息随后被 Looper 取出、分发,处理逻辑才运行在目标线程上。这不是把当前调用栈搬到另一条线程,而是通过队列交接一次工作。
多个 Handler 可以绑定同一个 Looper,因此也会共享它的 MessageQueue。它们可以使用不同的回调,却仍然竞争同一条线程的处理时间:其中一个 Handler 的回调执行太久,其他 Handler 的消息也要等。

dispatchMessage() 也不一定最终调用 handleMessage()。对于 post(Runnable),它优先执行消息携带的 Runnable;否则先尝试 Handler 构造时传入的 Callback,未被处理时再进入 handleMessage()。这条分发路径见 AOSP Handler。
二、为什么只有一个消费者,也必须加锁
这个版本的 MessageQueue 使用 mMessages 指向单链表头。普通定时消息按 when 安排位置,when 使用 SystemClock.uptimeMillis() 的时间基准。队头投递和同步屏障属于需要额外考虑的情况。
假设现有链表是 A → C,发送线程要在中间插入 B,至少需要完成:
B.next = C;A.next = B;保护这两次赋值还不够。插入前要遍历链表、找到位置,遍历依赖的状态也不能在中途被另一个线程改掉。因此,临界区需要覆盖“查找位置 → 修改链接 → 更新相关状态”的整个过程。
消费者同样会修改链表。取走队头时要移动 mMessages;跳过屏障取异步消息时,还要修改前一个节点的 next。如果只有生产者加锁,消费者仍然可能与入队操作交错,破坏链表的一致性。
单消费者只排除了多个消费者之间的竞争,并没有排除它与生产者之间的竞争。
三、入队加锁
enqueueMessage() 使用 synchronized (this),这里的 this 是目标 MessageQueue。下列代码按普通无同步屏障队列简化,保留锁与插入操作的关系:
// 无同步屏障场景的控制流示意。boolean enqueueMessage(Message msg, long when) { if (msg.target == null) { throw new IllegalArgumentException("Message must have a target."); } synchronized (this) { if (msg.isInUse()) { throw new IllegalStateException("Message is already in use."); } if (mQuitting) { msg.recycle(); return false; } msg.markInUse(); msg.when = when; Message p = mMessages; boolean needWake = false; if (p == null || when == 0 || when < p.when) { msg.next = p; mMessages = msg; needWake = mBlocked; } else { Message prev; do { prev = p; p = p.next; } while (p != null && when >= p.when); msg.next = p; prev.next = msg; } if (needWake) { nativeWake(mPtr); } } return true;}锁内不只有指针修改,还包括检查 mQuitting、标记消息正在使用和记录执行时间。这些操作共同决定一条消息能否成为队列中的合法节点。
完整源码还会考虑同步屏障:当队头是屏障,新入队的异步消息可能改变下一条可执行消息,因此即使没有插到链表头部,也可能需要唤醒。可对照 AOSP MessageQueue.enqueueMessage。
对于普通、相同 when 的消息,插入遍历会越过已有的同时间节点。但多个生产者并发调用时,不能用各自“开始发送”的日志顺序推断最终入队顺序;真正完成插入的先后仍受锁竞争和调度影响。
四、出队:修改链表时持锁,等待和回调时不持锁

next() 在检查消息时间、查找候选消息和摘链时,也进入同一个 synchronized (this)。它与 enqueueMessage() 的锁对象相同,所以这些操作不会并发修改同一条链表。
更值得注意的是锁外的两件事:
| 操作 | 是否持有 MessageQueue 的锁 | 原因 |
|---|---|---|
| 查找插入位置、接入消息 | 是 | 保护链表和队列状态 |
| 检查并摘出到期消息 | 是 | 与入队、删除操作互斥 |
nativePollOnce() 等待事件 |
否 | 等待期间允许其他线程入队 |
dispatchMessage() 执行业务回调 |
否 | 回调不属于队列结构维护 |
nativePollOnce() 不会自动替 Java 释放监视器锁。它能在等待期间让其他线程正常入队,是因为源码把它安排在同步块之外。若持着队列锁无限期等待,发送方就可能无法进入队列,也无法通过正常入队流程唤醒消费者。
业务回调也在 next() 返回后执行,此时队列锁已经释放。因此,一条耗时回调虽然会阻塞该 Looper 的后续消费,其他线程通常仍能继续入队。结果可能是队列越积越长,而不是所有发送方都等着这条回调释放队列锁。
五、锁既提供互斥,也建立可见性
同一个监视器锁有两个作用:一是让临界区互斥执行,二是在释放锁与后续获取同一把锁之间建立可见性关系。发送方入队前完成的数据写入,可以通过队列的同步交接让消费者观察到。这一关系来自 Java 内存模型中的 happens-before 规则,见 JLS:线程与锁。
但这不意味着 Message 和它引用的任意业务对象都变成了线程安全对象。消息发出后,发送方若继续修改 msg.obj 指向的可变对象,就可能与接收方同时读写它。队列锁只覆盖交接过程,不覆盖之后的业务访问。
Message msg = handler.obtainMessage(1, result);handler.sendMessage(msg);// 发送后不要再改写 msg,也不要无同步地修改 result 的共享状态。通常应传递不可变结果、快照,或明确移交独占使用权。若必须共享可变数据,应另外设计同步方式。
同一个 Message 实例也不能同时发送到多个 Handler。isInUse() 是误用检查,不是允许任意共享 Message 的并发协议;Handler 设置 target 等字段的动作也不能靠不同队列各自的锁统一保护。
六、线程安全不等于准时执行
队列锁会带来竞争,链表越长,查找插入位置也可能越慢。但 Handler 延迟任务不准时,并不只有这一种原因:前面的回调未完成、系统调度、设备休眠、同步屏障和任务积压,都可能推迟执行。
postDelayed(task, 1000) 是按 uptime 时间基准安排执行时刻,不是一个一秒钟后必定执行的实时承诺。深度休眠不计入 uptime,按墙上时钟观察,等待时间还可能更长。
因此,换成无锁队列也不能保证绝对准时。数据结构只影响其中一部分开销,无法消除线程调度和业务执行时间。对于持续产生的刷新任务,更直接的做法往往是合并重复请求、限制待处理工作量,并缩短每次回调。
七、理解 synchronized,要先确认锁对象
同步语法相似,互斥范围却由对象身份决定:
| 写法 | 锁对象 |
|---|---|
实例方法上的 synchronized |
当前实例 this |
静态方法上的 synchronized |
声明该方法的类对象 |
synchronized (lock) |
表达式 lock 指向的对象 |
两个不同 Handler 各自锁住自己,无法保护它们共享的 MessageQueue。同样,实例锁与类锁不会因为属于同一个类就自动互斥;保护同一份状态的访问必须遵守一致的锁规则。
本文由 AgentWorkFlow 生成