CPU 与线程问题:负载、锁与调度
2026/8/9大约 4 分钟性能与稳定性Android 14CPU锁调度线程
CPU 与线程问题:负载、锁与调度
上一章看了冷启动。这一章回答:运行期卡顿里,CPU 负载、锁竞争、调度延迟怎么区分, 怎么从时间线认出“谁在占 CPU、谁在等锁”?
线程处于 Runnable 但迟迟得不到 CPU,说明要查调度与系统负载;处于阻塞态要沿锁、futex、 Binder 或 I/O 查等待对象;持续 Running 才需要判断是有效计算还是忙等。三者修复方向不同。
运行、可运行与阻塞必须分开
| 常见误解 | 正确理解 |
|---|---|
| CPU 占用高就是线程在“干活” | 可能是忙等(自旋)、锁重试或无效轮询 |
| 锁等待会显示为 CPU 占用高 | 等锁的线程通常是阻塞状态,占 CPU 的是持锁者 |
| 线程多就能并行加速 | 线程过多反而增加调度与锁竞争,核心数才是上限 |
核心结论
- 线程状态区分三类问题:R(运行)、R+(可运行等调度)、S/D(阻塞等待)。
- 锁等待看持锁线程:找到持锁者,再看它为什么慢。
- 调度延迟看核心数与负载:多核满载时低优先级线程被延后。
- 线程池不是越大越好:超过核心数只会增加竞争与切换。
- Perfetto 的 sched 轨道能同时显示等待者与持锁者,是定位主战场。
三类问题的时间线特征
逐步解释:
- R:线程在跑,要看是真计算还是忙等。
- R+:可运行但没被调度,核心数或调度优先级问题。
- S/D:阻塞等待,要找到它在等什么(锁、I/O、Binder)。
- 统一收口:用 sched 轨道找到持锁/等待双方。
锁竞争为什么难查
等锁线程的栈只显示“在等”,不显示“等谁”。要打开持锁线程的栈,比对两边的同步对象。 Perfetto 可以同时展示两个线程的轨道,把“等待者”和“持锁者”放在同一时间轴上, 一眼看出谁拖慢了谁。
源码证据
阅读目标:确认线程状态的可观测来源。
正文指针:内核 /proc/<pid>/task/<tid>/stat 的状态字段; Thread.java 与 ReentrantLock 等同步对象的栈。
// /proc/<pid>/task/<tid>/stat(伪代码)
R running
S sleeping(可中断)
D uninterruptible sleep这段代码证明: 状态由内核提供,Perfetto 的 sched 轨道直接呈现;结合线程栈即可 区分“在跑、等调度、等锁/等 I/O”。
验证与排障
环境:开发设备或模拟器;以下命令不需要 root。
adb shell top -H -p <pid>
adb shell cat /proc/<pid>/task/*/stat 2>/dev/null | awk '{print $1, $3}'
adb shell perfetto -o /data/misc/perfetto-traces/trace.perfetto-trace \
-t 10s sched freq idle atrace am viewtop 看线程 CPU;stat 看状态分布;Perfetto 还原时间线。先确认线程是 R、R+ 还是 S/D,再决定查忙等、调度还是锁。
常见误区
- “CPU 高就是有效干活”:可能是忙等或无效轮询。
- “等锁的线程 CPU 高”:等锁通常阻塞,烧 CPU 的是持锁者重试。
- “线程越多越快”:超过核心数只会增加竞争。
- “一次 top 快照就能定位”:要看趋势与时间线。
延伸问题
- 自旋锁与阻塞锁在 trace 里如何区分?
- Binder 线程池占满如何表现为调度/等待问题?
- 如何用 Perfetto 对比持锁线程与等待线程的时间线?
- 低功耗策略(变频)如何影响调度延迟?
源码入口
| 文件 | 关键位置 | 作用 |
|---|---|---|
/proc/<pid>/task/<tid>/stat | 状态字段 | 线程状态 |
ReentrantLock.java | 锁实现 | 锁竞争 |
| 内核 sched 事件 | 调度轨迹 | 时间线 |
Thread.java | 线程创建 | 线程模型 |
公共路径:libcore/ojluni/src/main/java/java/util/concurrent/locks/ 与内核 sched。 行号以 r75 检索为准。
进程直接退出时应切换证据类型,见 崩溃与稳定性。