Handler 运行流程源码解析(六):MessageQueue 如何保持各线程添加消息的线程安全?

文章内容

本文内容基于艾文老师(小米)的《06-MessageQueue如何保持各线程添加消息的线程安全?》讲解视频整理

多个线程可以同时向一个 Handler 发消息,也可以通过不同 Handler 向同一个 Looper 投递任务。发送方各自运行,最终却要修改同一个 MessageQueue:谁负责防止消息丢失、链表断开或队列状态混乱?

答案是队列。在本文分析的实现中,MessageQueue 使用自身对象的监视器锁,把消息链表的插入、摘除和清理放进同一套互斥规则中。 发送方可以有多个线程,消费者通常只有 Looper 所在线程,但双方都需要遵守这套规则。

一、跨线程消息投递与消费

Handler、Looper 和 MessageQueue 的分工可以先用一条调用链串起来:

plain text
发送方线程  Handler.sendMessage / post    → Handler 设置 Message.target 等字段    → MessageQueue.enqueueMessage Looper 所在线程  MessageQueue.next    → Handler.dispatchMessage    → Runnable / Handler.Callback / handleMessage

调用 sendMessage() 时,入队代码仍在发送方线程上执行。只有消息随后被 Looper 取出、分发,处理逻辑才运行在目标线程上。这不是把当前调用栈搬到另一条线程,而是通过队列交接一次工作。

多个 Handler 可以绑定同一个 Looper,因此也会共享它的 MessageQueue。它们可以使用不同的回调,却仍然竞争同一条线程的处理时间:其中一个 Handler 的回调执行太久,其他 Handler 的消息也要等。

Handler 消息投递与消费链

dispatchMessage() 也不一定最终调用 handleMessage()。对于 post(Runnable),它优先执行消息携带的 Runnable;否则先尝试 Handler 构造时传入的 Callback,未被处理时再进入 handleMessage()。这条分发路径见 AOSP Handler。

二、为什么只有一个消费者,也必须加锁

这个版本的 MessageQueue 使用 mMessages 指向单链表头。普通定时消息按 when 安排位置,when 使用 SystemClock.uptimeMillis() 的时间基准。队头投递和同步屏障属于需要额外考虑的情况。

假设现有链表是 A → C,发送线程要在中间插入 B,至少需要完成:

java
B.next = C;A.next = B;

保护这两次赋值还不够。插入前要遍历链表、找到位置,遍历依赖的状态也不能在中途被另一个线程改掉。因此,临界区需要覆盖“查找位置 → 修改链接 → 更新相关状态”的整个过程。

消费者同样会修改链表。取走队头时要移动 mMessages;跳过屏障取异步消息时,还要修改前一个节点的 next。如果只有生产者加锁,消费者仍然可能与入队操作交错,破坏链表的一致性。

单消费者只排除了多个消费者之间的竞争,并没有排除它与生产者之间的竞争。

三、入队加锁

enqueueMessage() 使用 synchronized (this),这里的 this 是目标 MessageQueue。下列代码按普通无同步屏障队列简化,保留锁与插入操作的关系:

java
// 无同步屏障场景的控制流示意。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 指向的可变对象,就可能与接收方同时读写它。队列锁只覆盖交接过程,不覆盖之后的业务访问。

java
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 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Handler 运行流程源码解析(六):MessageQueue 如何保持各线程添加消息的线程安全? · Yinchen 的博客