Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MessageQueue 与锁竞争:从 DeliQueue 到系统等待链

Android 17 没有改变 Handler、Looper 和同步屏障对应用呈现的基本语义,改变的是 MessageQueue 内部的并发结构。对于运行在 Android 17 且 targetSdkVersion >= 37 的应用,平台默认启用 DeliQueue:生产者不再与 Looper 争用同一个 Java 监视器锁(monitor),而是先把消息压入无锁栈,再由 Looper 整理到自己独占的最小堆。最小堆是一种能快速取出最早到期消息的树形数据结构。

这项改造解决的是队列操作的锁竞争,不会让界面布局(layout)、绘制(draw)、数据库查询或 Binder 调用自动变快。分析卡顿时,可以把一轮消息处理分成三段:

  1. 入队:生产者通过 Handler 把 Message 放进队列。
  2. 出队:Looper 等待并选择下一条到期消息。
  3. 分发:Handler.dispatchMessage() 执行回调或业务代码。

DeliQueue 直接优化前两段的队列管理。第三段如果耗时,仍然要沿业务调用栈继续排查。

本文先用 DeliQueue 解释 MessageQueue 如何减少生产者与 Looper 的结构性争锁,再把视角扩展到 Java Monitor、futex、Binder 与 system_server 锁。两部分共享同一个诊断问题:等待链中究竟哪个节点无法推进。分析时先区分消息尚未得到执行,还是执行线程已经运行但正在等待其他资源。

Looper 队列、唤醒与 DeliQueue

1. MessageQueue 在事件循环中的位置

线程调用 Looper.prepare() 后,会得到一个 Looper 和与之绑定的 MessageQueue。随后,Looper.loop() 构成事件循环,持续取出并分发消息,直到队列退出。Android 17 的 Looper.loopOnce() 仍然把“出队”和“执行”分成两个明确步骤:

// frameworks/base/core/java/android/os/Looper.java
// AOSP android-17.0.0_r1,省略观测与日志代码
private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    Message msg = me.mQueue.next(); // 可能阻塞
    if (msg == null) {
        return false;
    }

    msg.target.dispatchMessage(msg);
    // ...
    msg.recycleUnchecked();
    return true;
}

next() 返回前,关注点是消息排序、屏障、唤醒和队列并发;dispatchMessage() 开始后,才进入 Handler.handleMessage() 或 Runnable.run()。因此:

  • 主线程在 MessageQueue.next() 附近出现监视器锁竞争(monitor contention),才可能是旧队列的锁竞争。
  • dispatchMessage() 下方的 layout、I/O 或 Binder 调用耗时很长,说明消息执行缓慢,不能归因于 MessageQueue 长时间持锁。
  • 一条消息执行太久会推迟下一次 next(),但这是时间上的连锁影响,不能倒推出队列内部的锁持有时间变长。

2. 旧实现的问题:一把锁保护一条有序链表

传统 MessageQueue 使用一条按执行时间 when 排序的单链表保存消息。enqueueMessage()、next()、移除消息和同步屏障操作都要进入同一个 synchronized (this) 临界区,也就是同一时刻只允许一个线程修改受保护状态。

这种设计容易理解,也适合消息量较小、生产者不多的场景,但有两个结构性成本:

  • 插入最坏为 O(N):队列中有 N 条消息时,新消息最坏要检查全部节点,才能找到按 when 排序的位置。
  • 生产者与消费者互斥:后台线程入队时,可能挡住正在取消息的主线程;主线程检查队列时,也可能挡住生产者。

一次短暂加锁通常问题不大,锁竞争与调度叠加后却可能形成优先级反转。例如:

  1. 后台低优先级线程取得 MessageQueue 的监视器锁。
  2. 中优先级线程抢占 CPU,使后台线程暂时无法继续运行和释放锁。
  3. 高优先级 UI 线程回到 next(),却必须等那个后台线程。

UI 线程表面上被低优先级线程阻塞,实际等待时间还被中优先级任务放大,这就是优先级反转。Perfetto 中常见的证据是主线程出现 monitor contention with ...,并且能定位持锁线程和加锁代码位置。

哪些业务更容易暴露旧结构的上限

一个容易漏看的入口是“切回主线程”本身。后台线程不需要直接改 UI;只要它调用 Handler.post(),或经 runOnUiThread()、RxJava 主线程调度、协程主线程调度等封装把回调送到主线程,它就是在给主 Looper 增加生产者压力。[来源: https://juejin.cn/post/7682633827692658740]

对旧实现来说,这类入口最终会汇聚到同一条 MessageQueue 入队路径;对 DeliQueue 来说,优化的也是这段投递与排序的结构,而不是回调自身的工作量。[已验证: frameworks/base/core/java/android/os/CombinedDeliMessageQueue/MessageQueue.java @ android-17.0.0_r1]

  • 多个后台线程高频向主线程 post()。
  • 队列已经积压大量延时消息,生产者需要在长链表中寻找插入位置。
  • 大范围调用 removeCallbacksAndMessages() 或各种 removeMessages() 重载。
  • 同步屏障存在时,Looper 需要越过同步消息继续寻找可执行的异步消息。

“主线程很忙”不等于“MessageQueue 锁竞争”。只有性能轨迹中出现对应的锁等待,或 A/B 测试能稳定复现差异,才能把问题归到队列同步结构。

3. next() 不只是取链表头

无论新旧实现,next() 都要同时处理等待、消息时序、同步屏障和空闲回调。

3.1 原生轮询:没有到期消息时让线程休眠

MessageQueue 的 Java 层通过 JNI(Java 与原生代码的调用桥梁)调用 nativePollOnce()。原生层的 MessageQueue 再交给 libutils::Looper,等待文件描述符事件或超时;Linux 实现最终使用 epoll 统一等待多个事件源。新消息可能改变下一次唤醒时间,入队线程便通过 nativeWake() 唤醒 Looper。

Android 17 仍保留这条链路:

MessageQueue.next()
  └─ nativePollOnce()
      └─ android_os_MessageQueue.cpp
          └─ libutils::Looper::pollOnce()/pollInner()
              └─ epoll_wait()

DeliQueue 不会让 Looper 循环占用 CPU 检查消息。没有可执行消息时,Looper 仍然阻塞在原生轮询中;变化主要发生在 Java 层共享队列的组织方式和唤醒协调上。

3.2 同步屏障:暂缓同步消息,让异步消息越过

同步屏障也是一条 Message,区别是 target == null。屏障生效时:

  • 同步消息即使已经到期,也暂时不能分发。
  • 到期的异步消息可以越过屏障。
  • 移除屏障后,同步消息恢复正常选择。

Choreographer 的调度路径会使用异步 Handler 或异步消息,从而在渲染调度需要时越过屏障。这里的“异步”不表示启动新线程;消息仍由原 Looper 线程执行,只是在同步屏障存在时可以优先通过。

3.3 IdleHandler:队列准备休眠时执行

MessageQueue.IdleHandler 从 API 1 起就存在。当队列没有立即可执行的消息、准备等待下一条消息时,Looper 可以调用已注册的 IdleHandler。回调返回 false 时会被移除,返回 true 时继续保留。

IdleHandler 与同步屏障解决的是两类问题:

  • 同步屏障决定“当前哪些消息可以越过”。
  • IdleHandler 决定“没有立即可执行消息时,是否做一点空闲工作”。

IdleHandler 运行在 Looper 所在线程,耗时操作照样会阻塞后续消息,不能把它当后台执行器。

4. Android 17 如何决定使用 DeliQueue

系统版本达到 Android 17,并不表示所有应用都会启用 DeliQueue。普通应用的默认切换还受目标 SDK 约束,也就是应用适配的平台行为版本。

在 android-17.0.0_r1 中,兼容性变更定义为:

// CombinedDeliMessageQueue/MessageQueue.java
@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
public static final long USE_NEW_MESSAGEQUEUE = 421623328L;

BAKLAVA 对应 Android 16 / API 36,因此 @EnabledAfter 把默认启用边界放在 API 37。应用进程启动时,这个兼容性判断会沿下面的路径传递:

PlatformCompat.getUseDeliQueue(appInfo)
  └─ ProcessList 启动应用进程
      └─ ZygoteProcess 添加 --use-deliqueue=<boolean>
          └─ ActivityThread.main() 解析参数
              └─ MessageQueue.setUseDeliQueue(...)
                  └─ Looper.prepareMainLooper() 创建队列

ActivityThread.main() 特意在 Looper.prepareMainLooper() 之前设置该值。MessageQueue 内部把选择保存为进程级静态状态;同一进程里的 MessageQueue 走同一种实现。

当前的 CombinedDeliMessageQueue/MessageQueue.java 同时保留 DeliQueue 和旧版(legacy)两条路径。下面的分支根据进程级状态选择实现:

Message next() {
    if (sUseDeliQueue) {
        return nextDeliQueue();
    } else {
        return nextLegacy();
    }
}

所以,“Android 17 源码只有 DeliQueue”与“每个 Android 17 应用都使用 DeliQueue”都不准确。平台用同一个组合类承载两种实现,再按进程启动时确定的兼容性结果选择路径。系统进程、测试环境和功能开关(feature flag)还有平台内部的启用入口,普通应用不应把这些内部条件当作稳定 API。

5. DeliQueue 的数据结构

DeliQueue 把“并发提交”和“按时间排序”分开处理:

生产者线程
  └─ CAS push
      └─ MessageStack(共享 Treiber 栈)

Looper 线程
  ├─ heapSweep():把新消息纳入堆
  ├─ mSyncHeap:同步消息与 barrier
  └─ mAsyncHeap:异步消息

这里没有由所有线程共同修改的并发优先队列。线程间共享的部分只保留无锁栈和少量原子状态,两个 MessageHeap 的实际增删和排序则只由 Looper 线程执行。

5.1 入队:Treiber 栈与 CAS

MessageStack 的源码注释将其定义为 “Treiber stack of Message objects”。Treiber 栈是一种后进先出的无锁栈,生产者通过比较并交换(Compare-And-Set,CAS)原子更新栈顶。入队路径把新节点发布到栈顶时使用 release CAS;后续的 isQuitting()、heapSweep() 和删除遍历再通过 acquire 读取当前栈顶或空闲链表,以保证节点内容在线程间可见:

// frameworks/base/core/java/android/os/MessageStack.java
// AOSP android-17.0.0_r1,省略退出哨兵判断
do {
    current = mTopValue;
    m.next = current;
} while (!sTop.weakCompareAndSetRelease(this, current, m));

CAS 失败说明栈顶已被其他线程改变,当前线程会重新读取并重试。它避免了旧实现中“先取得全局监视器锁才能入队”的互斥等待,但不代表每次入队只执行一条指令:高竞争下仍可能多次重试,还要处理消息计数、插入序号和原生唤醒协调。

调用线程的提交路径是 O(1),不随队列长度线性增长;随后 Looper 把消息放入最小堆时,仍要付出 O(log N) 的排序成本。排序成本没有消失,只是从“生产者在带锁链表中线性查找”改为“生产者快速提交,由单个消费者集中排序”。

5.2 出队:Looper 独占两个最小堆

MessageStack.heapSweep() 从当前栈顶遍历尚未处理的消息,建立供清理使用的反向链接,并按消息类型放入:

  • mSyncHeap:同步消息和同步屏障。
  • mAsyncHeap:异步消息。

MessageHeap 是用数组实现的最小堆,主要按 when 排序;普通消息执行时间相同时,再用插入序号 insertSeq 保持 FIFO。sendMessageAtFrontOfQueue() 是例外,源码用递减的负序号让队首消息按 LIFO 顺序排列。只有 Looper 线程会实际调整堆结构,因此每次向上或向下调整时不需要再取得 Java 锁。

下一条消息的选择仍遵守旧语义:

  1. 没有生效的同步屏障时,从已到期候选中选择应最先执行的消息。
  2. 同步屏障生效时,同步消息被暂缓,只能选择到期的异步消息。
  3. 没有消息到期时,计算等待时间并回到 nativePollOnce()。

DeliQueue 没有引入业务优先级、机器学习调度或新的线程优先级策略。应用能观察到的主要排序依据仍是 when、同时间的插入顺序、同步屏障和异步标记。

5.3 移除:先做逻辑删除,再由 Looper 清结构

removeMessages() 这类 API 按 Handler、what、obj 或 Runnable 匹配消息。因为 API 要删除所有匹配项,查找仍可能遍历已有消息,整体上不能算作 O(1)。

找到目标后,DeliQueue 不让任意线程直接重排堆,而是分三步:

  1. 对 Message.flags 做 CAS,设置 FLAG_REMOVED,完成逻辑删除。
  2. 清理会造成对象滞留的引用字段,并把墓碑节点(tombstone,表示消息已删除但节点尚未物理移除)放进另一条无锁空闲列表(freelist)。
  3. Looper 在 drainFreelist() 中把节点从栈和相应的堆里物理移除。

非 Looper 线程只负责把消息标记为无效,Looper 再清理自己独占的数据结构。读取线程即使短暂看到墓碑节点,也会根据已删除标记忽略它。

5.4 DeliQueue 为什么不复用 Message 池

Treiber 栈需要处理 ABA 问题:线程第一次看到栈顶是对象 A,暂停期间 A 被移除、复用,又重新成为栈顶;如果只比较对象引用,栈顶看起来仍是 A,线程就可能误判为“没有变化”。

单指针 CAS 本身无法消除 ABA。DeliQueue 的做法是把 ABA 的处理与对象生命周期绑在一起:进入并发队列的 Message 可能仍被某次删除遍历引用,因此不能立即回收到全局池,再作为另一条新消息复用。

源码中的行为很直接:

public static Message obtain() {
    if (!MessageQueue.getUseConcurrent()) {
        synchronized (sPoolSync) {
            // legacy 路径才尝试从全局池取 Message
        }
    }
    return new Message();
}

在 DeliQueue 路径中,recycleUnchecked() 会清除 obj、callback、data 等引用,但不会把对象放回共享消息池,也不会把并发删除所需的标志位和链路当作普通新消息状态复用。这样可以避免“节点对象被回收后以新身份重新出现”的经典 ABA 场景,代价是比旧版路径产生更多 Message 分配。评估收益时应同时观察锁竞争、对象分配速率和垃圾回收(GC),不能只看队列操作时长。

5.5 睡眠、唤醒与退出也要无竞争地协同

消息容器之外还要处理唤醒竞争。生产者入队时可能让一条更早的消息成为新队首,需要唤醒正在执行原生轮询的 Looper;Looper 准备休眠时,也可能恰好有另一个线程入队。

DeliQueue 使用原子等待状态(wait state)协调“预计睡到何时”和“期间发生过多少次需要重新判断的事件”,避免遗漏唤醒信号。同步屏障也有独立的原子状态,用来判断新增异步消息是否需要唤醒消费者。

退出阶段的竞争更敏感:其他线程可能正在通过 mPtr 调用原生唤醒,而 Looper 正准备销毁原生对象。Android 17 使用带退出标志的引用计数,确保仍有线程使用时不会销毁 mPtr 指向的对象。quitSafely() 仍然只处理已经到期的消息并移除未来消息;销毁原生对象则要等正在使用它的线程离开临界阶段。

6. “无锁 MessageQueue”不代表整个类没有锁

官方把 DeliQueue 称为无锁(lock-free)MessageQueue,因为消息的核心并发提交、检查和移除不再依赖旧的单一全局监视器锁。这里的“无锁”描述一种进展保证:即使某个参与线程暂停,其他线程仍能继续完成操作。

但 android-17.0.0_r1 的组合实现仍能看到:

  • mIdleHandlersLock:保护 IdleHandler 集合。
  • mFileDescriptorRecordsLock:保护文件描述符监听记录。
  • 旧版路径里的 synchronized (this)。
  • 原生轮询、唤醒和退出阶段的协调。

准确的表述是:DeliQueue 消除了旧 MessageQueue 核心消息路径上的单一全局监视器锁,并使用无锁共享结构与 Looper 私有堆协作。若把它理解成“MessageQueue 内任何操作都不加锁”,就会与源码冲突。

7. 同步屏障与 Choreographer:语义不变,容器变了

从渲染角度看,新旧实现的差别可以压缩为:

旧实现
生产者 enqueue ─┐
Looper next     ├─ 同一个 monitor + 有序链表
移除/barrier    ┘

Android 17 DeliQueue
生产者 enqueue ── CAS 压入 MessageStack
Looper next    ── sweep + 私有 sync/async 最小堆
移除         ── CAS tombstone + Looper 延迟清理

图中的 enqueue、next、sweep 分别表示入队、取下一条消息和把新消息整理进堆;sync/async、barrier、tombstone 分别是同步/异步、同步屏障和逻辑删除标记。屏障行为没有改变:同步消息等待,异步消息可以越过。变化在于生产者提交消息时,不再为了修改同一条链表而与 Looper 互斥。

这会改善 VSYNC-app 到 doFrame() 之间因为队列锁竞争造成的抖动,但不能保证每一帧都更快:

  • doFrame() 中的测量、布局或绘制(measure/layout/draw)太慢时,DeliQueue 无法修复。
  • 主线程被别的应用锁、Binder 或调度延迟挡住,仍要查各自根因。
  • 队列积压来自业务过量投递时,新结构能降低管理成本,却不会替应用丢弃无意义工作。

8. 官方性能数字应该怎样解读

外部技术文章常把 DeliQueue 的收益压缩成“多线程入队速度提升 5000 倍、主线程锁等待减少 15%”这类结论。[来源: https://juejin.cn/post/7682633827692658740] 这些数字可以引用,但必须回到官方博客的 Impact 边界:5,000× 是合成高竞争基准里“多线程向繁忙队列插入”的最高值,15% 是 Google 内部 beta 测试 Perfetto 轨迹中的应用主线程锁竞争耗时下降。[已验证: https://android-developers.googleblog.com/2026/02/under-hood-android-17s-lock-free.html]

Android Developers Blog 给出了 DeliQueue 的内部验证结果:

指标官方结果适用边界
多线程向繁忙队列插入最高 5,000×合成高竞争基准,不代表普通应用
App 主线程锁竞争耗时降低 15%Google 内部测试用户的 Perfetto 轨迹
App 掉帧降低 4%同批内部测试设备与工作负载
System UI / Launcher 交互掉帧降低 7.7%同批内部测试设备与工作负载
启动到首帧 P95缩短 9.1%同批内部测试设备与工作负载

5,000× 来自多线程向已经繁忙的队列插入消息的极端合成基准。官方没有在文章中公开完整线程数、消息规模、设备与参数,不能把它写成“应用普遍快 5,000 倍”。15% 和掉帧数据也描述 Google 的内部样本,不是 Android 17 对每台设备的性能承诺。

项目自己的结论至少要记录:

  • 设备和 Android 构建版本。
  • targetSdkVersion 与兼容开关状态。
  • 生产者线程数、消息量和队列积压程度。
  • Perfetto 配置、操作步骤、样本数与统计口径。
  • 除队列外的 CPU、GC、Binder 和帧流水线变化。

9. Android 17 迁移风险

9.1 反射 mMessages 得不到真实队列

为了兼容已有二进制,Android 17 仍保留私有字段 mMessages。DeliQueue 路径不使用它,官方迁移文档明确说明该字段会一直是 null。任何通过反射遍历 mMessages 的测试、监控或调试工具都不再可靠。

不要改为反射 MessageStack 或 MessageHeap。这些同样是私有实现,后续版本可以继续变化。测试代码应迁移到公开或测试专用 API。

9.2 测试框架版本

官方给出的最低建议是:

  • Espresso 3.7.0 或更高版本。
  • Robolectric 4.17 或更高版本,并从 @LooperMode(LEGACY) 迁移到 @LooperMode(PAUSED)。
  • 设备端插桩测试(instrumentation test)使用 TestLooperManager,包括 Android 16(API 36)引入的 peekWhen()、poll() 等能力,不再依赖 MessageQueue 私有字段。

9.3 用兼容性开关做同版本 A/B

在可调试应用上,可以执行:

adb shell am compat enable USE_NEW_MESSAGEQUEUE com.example.app
adb shell am force-stop com.example.app

adb shell am compat disable USE_NEW_MESSAGEQUEUE com.example.app
adb shell am force-stop com.example.app

每次切换后重新启动应用。实现选择发生在进程初始化、主 Looper 创建之前;只切开关而保留旧进程,无法得到可信对照。

A/B 结果的解释也要克制:

  • 关闭后崩溃消失:优先排查反射和测试工具对内部结构的假设。
  • 开启后监视器锁竞争消失:说明旧队列锁是原链路的一部分。
  • 两边 dispatchMessage() 都很长:继续修业务代码,别把它算成 DeliQueue 问题。

兼容开关用于开发验证和故障隔离,不应成为应用长期依赖的产品配置。

10. 用 Perfetto 验证,避免凭体感归因

第一步:确认等待发生在哪里

旧实现的典型证据是主线程出现 monitor contention with ... 轨迹区段,阻塞方法指向 MessageQueue。官方文章给出的 PerfettoSQL 使用 android_monitor_contention 表,并以 short_blocked_method LIKE "%MessageQueue%" 筛选主线程等待。

主线程仅处于休眠(Sleeping)状态不能证明存在锁竞争。它可能只是正常阻塞在 nativePollOnce(),等待下一条消息;只有结合竞争区段、持锁者和调用点,才能确认是 Java 监视器锁。

第二步:分开统计队列等待和消息执行

  • 队列侧:MessageQueue 监视器锁竞争次数、总时长、P95/P99,以及持锁线程。
  • 执行侧:Looper/Handler 轨迹区段下的具体回调、doFrame()、Binder、I/O 和锁。
  • 帧侧:实际帧时间线、丢帧原因,以及 VSYNC-app 到 doFrame() 的延迟。

Android 17 新实现消除旧队列的监视器锁竞争后,主线程仍可能处于可运行状态却拿不到 CPU,也可能在其他锁上阻塞。线程状态必须结合调度和调用栈一起检查。

第三步:做控制变量明确的 A/B

应在同一台 Android 17 设备、同一构建版本、同一 APK 和同一套操作脚本上,只切换 USE_NEW_MESSAGEQUEUE。每轮强制停止并冷启动进程,收集多份性能轨迹,再比较分位数。不能直接对比 Android 16 与 Android 17 两个完整系统,再把所有差异都归给 DeliQueue。

对 system_server 做平台开发时,Android 17 还提供 mq 轨迹事件(track event)分类,可在 Perfetto 配置中启用 MessageQueue 跟踪。分析普通应用时,仍应优先使用稳定的 Looper、调度、锁竞争和帧时间线证据,不要依赖隐藏实现字段。

版本与实现边界

版本MessageQueue 相关变化
Android 1.0(API 1)MessageQueue、Looper 与 IdleHandler 已存在
Android 4.1(API 16)Choreographer 使用同步屏障与异步消息组织渲染调度
Android 15(API 35)旧版主线仍是单链表加单一监视器锁
Android 16(API 36)公开源码出现组合版、并发版和旧版多种实现,处于优先为系统进程逐步启用的阶段
Android 17(API 37)DeliQueue 面向 targetSdkVersion >= 37 的应用默认启用;当前源码锚点为 CombinedDeliMessageQueue、MessageStack、MessageHeap 与扩展后的 Message

Android 16 的并发实现适合解释演进,不能替代 Android 17 的当前源码。Android 17 的 MessageStack 本身就是 Treiber 栈,不能描述成“用 MessageStack 替换 Treiber 栈”;真正的变化是并发原型最终采用共享 Treiber 栈、Looper 私有双堆、墓碑删除,以及完整的休眠和退出协调。

常见误区

“无锁就是没有任何 synchronized”

不是。核心消息路径不再依赖旧的全局监视器锁,但 IdleHandler、文件描述符记录和旧版路径仍有各自的锁。

“CAS 一定比锁快”

不是。竞争较少且临界区很短时,锁可能已经足够快;高竞争下 CAS 也会不断重试。DeliQueue 的收益来自重新设计 MessageQueue 的整体结构,不能简单归结为把每个 synchronized 替换成原子变量。

“MessageStack 解决了所有 ABA 问题”

不准确。MessageStack 就是 Treiber 栈。Android 17 通过禁止 DeliQueue 中的 Message 对象池复用、保留墓碑节点的生命周期,并限定只有 Looper 修改实际结构,来规避节点复用引发的 ABA 问题。

“新队列给渲染消息增加了更高业务优先级”

没有。同步屏障与异步消息语义沿用既有模型;DeliQueue 改的是并发容器和协调算法,不是应用任务优先级系统。

“主线程处于休眠状态就是队列锁竞争”

不成立。空闲 Looper 正常阻塞在原生轮询中。只有看到 android_monitor_contention、持锁线程和 MessageQueue 调用点,才能确认存在旧版监视器锁竞争。

结论

阅读 Android 17 MessageQueue,需要抓住两条边界。

第一,DeliQueue 优化的是入队和出队:生产者通过 CAS 把消息提交到 MessageStack,Looper 再把消息整理进自己独占的同步/异步最小堆;移除操作先做逻辑删除,再由 Looper 清理结构。同步屏障、异步消息、IdleHandler 和原生轮询的对外语义都没有改变。

第二,官方所说的无锁,指核心消息并发路径不再依赖旧的单一全局监视器锁;它不代表整个类没有锁,也不代表业务回调会自动变快。迁移时应检查反射和测试框架;性能分析时,要把队列等待与 dispatchMessage() 之后的业务执行分开,并通过同版本兼容开关和 Perfetto 证据完成 A/B 测试。

锁竞争的识别与治理

队列结构可以降低生产者竞争,但不能消除消息处理过程中的业务锁。锁持有者、等待者和调度状态需要在同一时间窗口内还原。

线程显示为等待(Waiting)或休眠(Sleeping),只能证明它当时没有在 CPU 上执行,不能直接证明发生了锁竞争。它可能在等待 Java 监视器锁、原生互斥锁(native mutex)、条件变量、Binder 回复、I/O 或定时器,也可能只是 Looper 正常阻塞在 epoll_wait(),等待新事件。

诊断锁问题时,先回答四个问题,不要从猜测“哪种锁更快”开始:

  1. 谁在等:主线程、RenderThread、Binder 工作线程,还是普通后台线程?
  2. 等什么:Java 监视器锁、原生同步原语、Binder 回复,还是正常事件?
  3. 谁能让它继续:实际持锁线程或服务端线程是谁?
  4. 这个线程为什么没有及时推进:正在运行、排队等待 CPU、阻塞在另一把锁,还是执行 I/O?

这四个答案拼起来,才是一条可修复的等待链。

1. 先把几类“等”分开

Android 性能轨迹中,下面五类等待最容易混淆。

类型常见入口主要实现首要证据
Java 监视器锁synchronized、Object.wait()ART Monitor / LockWordandroid_monitor_contention、等待者与持锁者的方法
Java 并发包ReentrantLock、Condition、LockSupport.park()抽象队列同步器(AQS)、线程挂起/唤醒、原生等待Java 调用栈、线程状态、相关业务埋点
原生锁pthread_mutex、std::mutex、条件变量bionic + futex原生调用栈、blocked_function、锁埋点
Binder IPC同步 AIDL、服务端线程池libbinder + Binder 驱动Binder 事务/回复、调用端/服务端线程状态
Looper 等待MessageQueue.next()Java 队列 + 原生 Looper + epoll是否有到期消息、nativePollOnce()、MessageQueue 锁竞争

它们都可能在内核线程状态里表现为睡眠,但优化方式完全不同。

  • Java 监视器锁要找到持有同一对象锁的线程。
  • ReentrantLock 不属于 ART Monitor,不能指望 android_monitor_contention 自动给出持锁者。
  • 原生 futex_wait 只说明线程进入了 futex 的内核等待路径,无法据此确认它对应互斥锁或启用了优先级继承。
  • 同步 Binder 调用的调用端等待服务端回复;服务端内部可能又阻塞在 Java 或原生锁上。
  • 空闲 Looper 睡在原生轮询中属于正常行为,无需消除。

2. ART Monitor:synchronized 在 Android 17 中怎样工作

Java 字节码使用 monitorenter 和 monitorexit 表达 synchronized。ART 在对象头的 32 位 LockWord(锁状态字)中记录轻量锁状态,必要时再关联完整的 Monitor 对象。

art/runtime/lock_word.h 在 android-17.0.0_r1 中定义了这些状态:

enum LockState {
  kUnlocked,
  kThinLocked,
  kFatLocked,
  kHashCode,
  kForwardingAddress,
};

这些枚举说明 LockWord 还要容纳对象身份哈希值和垃圾回收所用的转发地址。分析 synchronized 时,不能把对象头始终解释成持锁线程 ID 和重入计数。

2.1 无竞争路径:轻量锁

对象未加锁时,ART 可以把当前线程用于 Monitor 的线程 ID 和重入计数编码进 LockWord。线程通过原子更新取得轻量锁(thin lock);同一线程再次进入同一把锁时,只增加重入计数。

轻量锁的优势是,不必为每个使用过 synchronized 的对象都分配完整 Monitor。没有竞争时,这条路径很短,不会让线程休眠,也不会触发内核调度切换。

“无竞争的 synchronized 开销较小”,不表示任何 synchronized 都只执行一次比较并交换(CAS)。对象可能已经膨胀为重量级 Monitor,可能带有身份哈希值(identity hash code),也可能发生重入和其他运行时状态变化。性能结论要结合实际对象和竞争形态。

2.2 竞争、wait() 与身份哈希:Monitor 膨胀

art/runtime/monitor.cc 的源码注释列出需要完整 Monitor 的三类典型情况:

  • 出现真实竞争。
  • 在对象上调用 wait()。
  • 一个需要加锁的对象同时带有身份哈希值。

竞争线程遇到已被其他线程持有的轻量锁时,ART 会与持锁线程协调,并尝试把锁膨胀为重量级 Monitor。重量级 Monitor 会记录完整的持锁者、等待者、条件等待和诊断信息。

Object.wait() 的语义也不能简化成“睡一会”:

  1. 调用线程必须已经持有该对象的监视器锁。
  2. wait() 把线程放入等待集合,并释放监视器锁。
  3. notify() / notifyAll()、中断或超时使它具备继续条件。
  4. wait() 返回前还必须重新取得同一个监视器锁。

因此,被唤醒不等于马上执行。线程可能从“等通知”转成“等重新拿锁”。

2.3 kLongWaitMs 是日志阈值,不是统一的 Perfetto 阈值

Android 17 源码定义:

static constexpr uint64_t kDebugThresholdFudgeFactor =
        kIsDebugBuild ? 10 : 1;
static constexpr uint64_t kLongWaitMs =
        100 * kDebugThresholdFudgeFactor;

也就是发布(release)构建为 100ms,调试(debug)构建为 1000ms。这个常量服务于 ART 的长时间竞争告警;是否记录日志,还会受采样和持锁者方法能否解析等条件影响。

它不能用来推导以下结论:

  • “小于 100ms 的锁竞争不会进 Perfetto。”
  • “没有长等待日志就没有锁问题。”
  • “100ms 是 Android 所有锁的统一严重阈值。”

一帧在 60Hz 下只有约 16.7ms,几毫秒的主线程锁等待已经可能造成掉帧,远不到 ART 长等待日志的量级。

3. Futex:用户态快路径与内核慢路径

futex(fast userspace mutex)是让用户态原子状态与内核等待队列协作的基础机制,本身并不是某一种具体的 C++ 锁。没有竞争时通常只操作用户态内存;需要让线程休眠或唤醒时,才进入内核。

普通互斥锁(mutex)的典型流程如下:

尝试用户态原子加锁
  ├─ 成功:直接进入临界区
  └─ 失败:标记存在竞争,通过 futex 进入内核等待

解锁
  ├─ 没有 waiter:用户态完成
  └─ 存在 waiter:通过 futex 唤醒等待线程

bionic/libc/bionic/pthread_mutex.cpp 中未启用优先级继承(PI)的互斥锁,在发生竞争时调用 __futex_wait_ex(),释放有等待者的锁时调用 __futex_wake_ex()。没有竞争时,不需要每次都进入内核。

这也是为什么 blocked_function 里看到 futex_* 仍然不能直接下结论:

  • 它可能来自 pthread_mutex;
  • 可能来自条件变量;
  • 可能来自 ART 或其他运行时内部同步;
  • 可能是 Java 并发包最终触发的线程挂起(park);
  • 只能证明线程在某个 futex 等待点休眠,不能单凭函数名还原锁对象和持锁者。

要定位原生锁,通常还需要原生调用栈、业务锁埋点和同进程线程状态,或者在可复现环境中为该锁增加 Trace 区段。

4. 优先级反转与 PI-futex

优先级反转的经典链条是:

低优先级线程 L:持有锁
中优先级线程 M:持续占用 CPU
高优先级线程 H:等待 L 的锁

结果:H 的进度被 M 间接拖慢

即使 L 的临界区只执行 1ms,如果 L 长时间得不到 CPU,H 实际经历的等待时间也可能远大于 1ms。

4.1 优先级继承的作用

ACK android17-6.18-2026-06_r6 的实时互斥锁(rt-mutex)文档描述了优先级继承(Priority Inheritance,PI):

  • 高优先级等待者阻塞在 rt-mutex 上时,低优先级持锁者临时继承更高优先级;
  • 持锁者释放锁后撤销这次提升;
  • 持锁者又阻塞在另一把 rt-mutex 上时,优先级提升可以沿依赖链传播;
  • 等待者按优先级组织,同优先级使用先进先出(FIFO)顺序。

PI 缩短的是“高优先级线程被低优先级持锁者挡住,而持锁者又得不到 CPU”的时间。它无法缩短持锁者在临界区内执行的 I/O、长计算或同步 Binder 调用。

4.2 Android 17 bionic 怎样选择 PI 互斥锁

bionic 通过下面的属性显式启用优先级继承:

pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&mutex, &attr);

只有互斥锁属性显式选择 PTHREAD_PRIO_INHERIT,bionic 才会调用 __futex_pi_lock_ex() / __futex_pi_unlock(),由内核 PI-futex 和 rt-mutex 提升持锁者优先级。普通互斥锁仍走普通 futex 路径。

所以:

  • “Android 内核支持 PI-futex”只说明具备这项能力;
  • 判断“这把锁启用了 PI”,必须检查它的初始化属性;
  • 某线程停在 futex_wait,不能证明该锁走的是 PI 路径。

PI 不能替代合理的锁设计。临界区过大、锁顺序混乱、持锁执行 I/O 等问题,仍应从设计上修复。

5. Binder 等待与 Java 锁等待是两条不同路径

同步 Binder 调用跨越调用端(client)、Binder 驱动和服务端(server):

Client thread
  └─ binder transaction
      └─ Binder driver 选择/唤醒 server thread
          └─ Server Binder thread 执行服务代码
              └─ binder reply
                  └─ Client 继续执行

调用端等待回复时,并没有持有一把所谓的“Binder Java 监视器锁”。驱动使用自己的锁和等待队列管理事务、线程与工作项;服务端业务代码则可能再次遇到 Java 监视器锁或原生互斥锁。

一次同步 Binder 延迟可以拆成:

总墙上时间
= client 入驱动与排队
+ server thread 获得运行机会
+ server 业务执行
+ server 内部锁 / I/O / 嵌套 Binder 等待
+ reply 返回与 client 再次被调度

只看到调用端休眠在 Binder 相关内核函数上,无法判断哪一项耗时占主导。

5.1 Binder 有自己的优先级传播

drivers/android/binder.c 在当前 ACK tag 中包含:

  • binder_select_thread_ilocked():为进程中的工作选择等待线程;
  • binder_wakeup_thread_ilocked():唤醒具体线程,或通知进程需要增加线程;
  • binder_transaction_priority():根据事务和 Binder 节点(binder node)的限制,设置服务端处理线程的优先级。

这是 Binder 自身的事务调度机制,不等同于 PTHREAD_PRIO_INHERIT,也不会自动提升“服务端线程正在等待的某个 Java 监视器锁持有者”。如果 Binder 工作线程进入服务代码后又阻塞在业务锁上,仍要沿 Java 或原生锁的持有关系继续排查。

5.2 默认 15 不代表进程里固定只有 15 条 Binder 线程

Android 17 的 ProcessState.cpp 定义:

#define DEFAULT_MAX_BINDER_THREADS 15

初始化 Binder 驱动时,libbinder 通过 BINDER_SET_MAX_THREADS 把这个默认最大值设为 15。与此同时,startThreadPool() 会调用 spawnPooledThread(true),主动启动一条主线程池线程(main pooled thread)。

因此应该这样读:

  • 15 是 libbinder 传给驱动的默认“可按需请求启动的线程”上限;
  • 主动启动的主线程池线程要单独计入线程总数;
  • 业务线程也可以进入 Binder 调用上下文;
  • 服务可以在启动线程池前通过 API 配置不同上限。

所以,把 Binder 线程池概括成“固定 15 条”或“固定 16 条”都会误导性能分析。线程数只是容量的一部分;一个工作线程长时间持锁或执行缓慢 I/O,仍可能让其他事务排队。

6. system_server:Binder 慢经常只是表象

应用主线程调用 AMS、WMS 或 PMS 后长时间等待回复,常见根因位于服务端,而非 Binder 驱动本身:

  • Binder 工作线程等待 system_server 的全局对象锁;
  • 持锁线程在锁内执行长计算或磁盘 I/O;
  • 持锁线程在锁内发起另一个同步 Binder 调用;
  • 多把系统锁形成长等待链;
  • Binder 工作线程接近饱和,新事务迟迟没有线程处理。

分析时,应从调用端事务跟到服务端回复,再检查服务端线程的状态;如果它在等锁,就继续找持锁线程,不能停留在“Binder 调用耗时”这个表面结论。

6.1 Android 17 AMS 的 mGlobalLock 与 mProcLock

ActivityManagerService.java 当前源码明确给出:

final ActivityManagerGlobalLock mGlobalLock = ActivityManagerService.this;

private static final boolean ENABLE_PROC_LOCK = true;

final ActivityManagerGlobalLock mProcLock = ENABLE_PROC_LOCK
        ? new ActivityManagerProcLock() : mGlobalLock;

源码注释规定了锁的获取顺序:先取得 mGlobalLock,再取得 mProcLock;不能在只持有 mProcLock 时反向获取 mGlobalLock,否则可能形成死锁。

@CompositeRWLock({"mService", "mProcLock"}) 表达的是供静态分析和代码审查使用的组合锁契约,不会在运行时创建新的读写锁对象:相关状态的读取可由两把锁中的任意一把保护,写入通常要求同时持有两把锁。方法后缀也能提示调用者需要持有哪些锁:

  • LOSP:通常表示持有列出的任意一把锁;
  • LSP:通常表示同时持有服务全局锁和进程锁。

例如 Android 17 的 OomAdjuster:

@GuardedBy("mServiceLock")
void updateOomAdjLocked(@OomAdjReason int oomAdjReason) {
    synchronized (mProcLock) {
        updateOomAdjLSP(oomAdjReason);
    }
}

这里调用者已经持有 mServiceLock,随后再进入 mProcLock,符合从全局锁到进程锁的顺序。ProcessList.mLruProcesses 也标注为由组合锁保护。

这些源码能证明哪些状态由哪些锁保护,以及锁的获取顺序,却不能单独证明某个固定的性能提升比例。锁持有时间必须来自具体设备的性能轨迹;没有可定位的一手基准时,不应写“从 25ms 降到 8ms”或“吞吐提升 3 倍”。

7. 把 DeliQueue 放回等待链中

前半篇已经完整解释 DeliQueue 的旧锁、新栈与双堆实现;这里不重复数据结构,只提炼诊断意义。若 Android 17 / target API 37 上的证据表明生产者入队争锁明显下降,但消息仍然迟到,就应继续检查 Looper 前序任务、执行线程调度、业务锁、Binder 与 I/O。反过来,在旧实现或兼容开关未启用时,MessageQueue 自身的 monitor contention 仍可能是等待链中的关键节点。

DeliQueue 因此是一个有边界的去锁案例,而不是“无锁一定更快”的证明。Google 公布的 15% 锁竞争时间下降只描述其内部样本;CAS 重试、缓存行争用、内存分配和延迟清理仍需在目标负载中测量。

8. Perfetto:先找证据,再解释原因

8.1 Java 监视器锁

当前 Perfetto 标准库(stdlib)的模块名是 android.monitor_contention,查询表名是 android_monitor_contention。下面的 SQL 列出 system_server 中耗时最长的 30 次 Java 监视器锁竞争:

INCLUDE PERFETTO MODULE android.monitor_contention;

SELECT
  process_name,
  blocked_thread_name AS waiter,
  blocking_thread_name AS owner,
  short_blocked_method AS waiter_method,
  short_blocking_method AS owner_method,
  lock_name,
  dur / 1e6 AS wait_ms
FROM android_monitor_contention
WHERE process_name = 'system_server'
ORDER BY dur DESC
LIMIT 30;

查询结果能给出等待线程(waiter)、持锁线程(owner)、双方方法、源码位置、是否为主线程、锁名和等待时长。android_monitor_contention_chain 还能表达多段锁竞争之间的依赖关系;配套的线程状态表可以继续检查持锁者在锁内是正在运行(Running)、可运行但等待 CPU(Runnable),还是又阻塞在其他内核函数中。

注意两个边界:

  • 表中没有记录,不等于设备上完全没有竞争;性能轨迹配置、运行时采样和符号解析都会影响数据完整性;
  • 这张表针对 ART Java Monitor,不覆盖所有 ReentrantLock 和原生互斥锁。

8.2 原生 futex 等待

没有专门的锁埋点时,可以先从 thread_state.blocked_function 查找耗时较长的 futex 等待。下面的 SQL 列出最长的 50 个候选区间:

SELECT
  p.name AS process_name,
  t.name AS thread_name,
  ts.ts,
  ts.dur / 1e6 AS blocked_ms,
  ts.state,
  ts.blocked_function
FROM thread_state ts
JOIN thread t ON t.utid = ts.utid
LEFT JOIN process p ON p.upid = t.upid
WHERE ts.blocked_function GLOB '*futex*'
ORDER BY ts.dur DESC
LIMIT 50;

这一步只负责找候选。随后还要结合原生调用栈或源码,确认它对应互斥锁、条件变量(condvar)还是其他等待,并寻找真正能使其继续的线程。不能只根据 futex_wait 这个函数名,就断定发生了 Java 锁竞争。

8.3 Binder 调用端与服务端

Perfetto 的 android.binder 模块可以把事务与回复关联到调用端和服务端。下面的 SQL 按调用端总耗时列出最长的 30 笔同步 Binder 事务:

INCLUDE PERFETTO MODULE android.binder;

SELECT
  client_process,
  client_thread,
  server_process,
  server_thread,
  interface,
  method_name,
  is_sync,
  client_dur / 1e6 AS client_ms,
  server_dur / 1e6 AS server_ms
FROM android_binder_txns
WHERE is_sync
ORDER BY client_dur DESC
LIMIT 30;

找到慢事务后,可以再用 android_sync_binder_thread_state_by_txn 或 android_sync_binder_blocked_functions_by_txn 分解调用端与服务端的线程状态。若服务端处理区间叠加 Java 监视器锁竞争,就继续查询 android_monitor_contention 给出的持锁者。

“调用端时间长、服务端时间短”可能意味着事务排队、线程调度或回复返回延迟;“服务端时间长”也不等同于一直执行 CPU 计算,服务端可能大部分时间都在等待锁、I/O 或嵌套 Binder 调用。

9. 三个常见现场怎样推理

现场一:主线程出现 monitor contention with ...

  1. 在 android_monitor_contention 中找到等待者方法、持锁线程和持锁者方法;
  2. 查看持锁者在等待区间内的 thread_state;
  3. 如果持锁者处于 Runnable 状态却长时间没有运行,检查 CPU 压力与优先级反转;
  4. 如果持锁者又等待另一把锁或 Binder,继续沿依赖链排查;
  5. 回源码确认临界区,检查能否移出 I/O、IPC 或长计算。

现场二:主线程睡在同步 Binder 调用

  1. 用 android_binder_txns 找到服务端进程、线程和方法;
  2. 比较调用端与服务端区间,确认时间主要花在哪一端;
  3. 服务端工作线程若等待 Java Monitor,用竞争表找持锁者;
  4. 服务端工作线程若停在原生 futex,结合原生调用栈找具体同步对象;
  5. 多个事务都在排队时,检查工作线程是否饱和,以及某个长事务是否占用线程过久。

现场三:线程停在 futex_wait

  1. 先确认 Java 调用栈来自 ART Monitor、AQS/线程挂起,还是 JNI/原生代码;
  2. 检查是否有对应的 android_monitor_contention 事件;
  3. 没有 Java Monitor 证据时,按原生锁或条件等待调查;
  4. 找到锁的初始化代码,确认是否为 PI 互斥锁,不能根据函数名猜测;
  5. 如果这是正常的条件变量等待,就继续查“为什么条件迟迟没有满足”,无需优化 futex 本身。

10. 锁优化的工程顺序

10.1 先缩短临界区

最常见的有效改动,是把不需要维持共享状态一致性的工作移出锁:

  • 日志格式化和大对象构建;
  • 磁盘、网络和数据库 I/O;
  • 同步 Binder 调用;
  • 可以先在锁内复制输入、锁外计算、再在锁内提交结果的长计算;
  • 不需要共享状态保护的回调。

把代码移出锁前,要重新确认临界区保护的不变量,也就是共享状态始终必须满足的约束。盲目缩小范围可能引入竞态(check-then-act race):在“先检查”和“后操作”之间,共享状态已被其他线程改变。性能改善不能以破坏正确性为代价。

10.2 再减少共享状态

  • 用线程封闭(只允许一个线程访问状态)或消息传递代替共享可变对象;
  • 把一把全局锁拆成按对象、按分片(shard)或按子系统的锁;
  • 读多写少时考虑不可变快照、写时复制(copy-on-write)或读写分离;
  • 高频计数器考虑分片,减少多个 CPU 写入同一缓存行。

拆锁会引入锁顺序和跨锁一致性问题。应先写清哪些字段共同构成一个必须原子维护的不变量,再决定能否拆分。

10.3 明确锁顺序

跨多把锁时,项目应定义全局顺序,并在代码审查中阻止反向获取。Android 17 AMS 对 mGlobalLock → mProcLock 的注释就是这种契约。

超时只能限制单次等待时间,不能修复潜在死锁;tryLock() 获取失败后的状态恢复同样需要正确设计。

10.4 最终才比较锁类型或无锁结构

选择 synchronized、ReentrantLock、读写锁或无锁结构,应由需求决定:

  • 是否需要可中断的锁获取或超时;
  • 是否需要多个条件队列(Condition);
  • 是否接受公平锁按等待顺序分配锁带来的吞吐成本;
  • 读写比例和临界区大小;
  • 竞争线程数、优先级和 CPU 核心布局;
  • 失败重试与内存回收成本是否可控。

微基准要覆盖真实的竞争形态。只测试单线程获取/释放锁(acquire/release),无法预测主线程与多个后台生产者同时竞争时,高百分位请求的尾延迟会有多长。

10.5 增加 Binder 线程数不是首选修复手段

增加工作线程可能缓解排队,也可能让更多线程同时争用同一把全局锁,增加内存和调度压力。应先找最长事务、持锁 I/O、嵌套同步 IPC 和锁依赖链,再判断线程池容量是否不足。

版本与实现边界

版本可确认的同步相关变化阅读方式
Android 5.0(API 21)应用运行时切换到 ART,Java Monitor 分析以 ART monitor.cc / lock_word.h 为准不沿用 Dalvik 时代实现细节解释当前系统
Android 9(API 28)bionic 已提供 PTHREAD_PRIO_INHERIT 互斥锁属性接口只说明这项能力可用;具体锁是否启用仍要看初始化
Android 17(API 37)当前 bionic 的 PI 互斥锁走 PI-futex;当前 ACK 为 android17-6.18-2026-06_r6;DeliQueue 对目标 SDK 37 及以上应用默认启用平台、bionic 与内核源码按本知识库的统一锚点核对

Binder 默认线程配置在历史上容易被误传。在当前 Android 17 锚点下,DEFAULT_MAX_BINDER_THREADS 为 15,startThreadPool() 还会另行启动主线程池线程;这个数值并非进程的固定 Binder 线程总数。

常见误区

“Waiting / Sleeping 就是锁竞争”

不成立。Looper、条件变量、Binder、I/O 和定时器都会让线程睡眠。先找等待对象和唤醒条件。

“synchronized 一定比 ReentrantLock 慢”

不成立。它们的功能、运行时路径和竞争行为不同。没有实际工作负载和性能轨迹,单凭锁类型无法判断。

“看到 futex_wait 就找 Java Monitor 的持锁者”

不成立。应先看 android_monitor_contention 和调用栈。futex 是底层等待机制,不是 Java Monitor 的专属标志。

“内核支持 PI,所以 Android 关键锁都有优先级继承”

不成立。bionic 互斥锁必须使用 PTHREAD_PRIO_INHERIT 初始化;Binder 的事务优先级传播又是另一套机制。

“Binder 调用慢就是驱动慢”

只看到 Binder 调用耗时,通常证据还不够。服务端工作线程可能阻塞在业务锁、I/O、CPU 调度或嵌套 IPC 上。应先把调用端与服务端的时间线连起来。

“无锁一定更快”

不成立。CAS 重试、多个 CPU 核反复争用同一缓存行、垃圾回收(GC)和算法复杂度都可能成为新成本。DeliQueue 的价值来自针对具体队列结构的重构,不能只归结为“无锁”两个字。

结论

锁竞争分析要还原完整的等待链。Java Monitor 使用 android_monitor_contention 查找等待者与持锁者;原生互斥锁要从 futex 候选回到原生调用栈和初始化代码;Binder 要用事务与回复连起调用端和服务端;MessageQueue 还要区分正常的原生轮询与旧版监视器锁竞争。

找到持锁者后还要继续判断:它正在 CPU 上运行,还是处于 Runnable 状态却没有被调度?它是否阻塞在另一把锁、I/O 或 Binder 上?找到整条等待链中无法推进的节点后,缩短临界区、拆锁、调整线程模型、启用 PI 或采用无锁结构才有明确目标。

参考资料