Android 线程与线程池面试题深度解析(资深版)
本文基于 Android 14 / Java 17 源码,面向资深 Android 开发工程师,涵盖线程基础、线程池原理、Binder 线程池等核心知识点,深入源码层面解析设计思想与实现细节。
目录
一、线程基础篇
Q1:Android 为什么需要多线程?
答案
Android 应用的主线程(UI 线程)负责界面绘制(每 16ms 一次,保证 60fps)和用户交互(点击、滑动等)。任何耗时操作(网络请求、数据库读写、文件 I/O、大量计算)都会阻塞主线程,导致掉帧、卡顿,甚至 ANR(Application Not Responding,Activity 5 秒无响应 / BroadcastReceiver 10 秒无响应)。多线程将耗时任务移到后台线程,保证主线程的响应性。同时多线程可充分利用多核 CPU 提升计算效率。
流程图:
flowchart LR
A[主线程] --> B{任务类型}
B -->|UI 操作| C[立即执行]
B -->|耗时操作| D[子线程执行]
D --> E[结果通过 runOnUiThread / View.post 回传]
E --> C
源码示例:
// 简单后台线程
new Thread(() -> {
Bitmap bitmap = loadImageFromNetwork();
runOnUiThread(() -> imageView.setImageBitmap(bitmap));
}).start();
Q2:多线程为什么会有并发问题?
答案
多线程并发问题的根源是 共享资源的非原子性操作、缓存一致性 和 指令重排序:
- 可见性:一个线程修改了共享变量,其他线程不能立即看到(每个线程有自己的工作内存/缓存)
- 原子性:对变量的读-改-写操作(如
count++)可能被线程调度打断,分为读、改、写三步 - 有序性:编译器/CPU 可能重排序指令,导致多线程环境下代码执行顺序与预期不符
JMM 模型图:
flowchart LR
subgraph 主内存
M[共享变量 count=0]
end
subgraph 线程A
CA[工作内存 副本=0]
end
subgraph 线程B
CB[工作内存 副本=0]
end
M -->|read| CA
M -->|read| CB
CA -->|write| M
CB -->|write| M
示例:线程 A 和 B 同时读取 count=0,各自计算为 1 并写回,最终结果为 1 而非期望的 2 → 丢失更新。
Q3:JVM 内存模型(JMM)详细说明
答案
JMM(Java Memory Model)定义了线程与主内存之间的抽象关系,是理解并发问题的理论基础:
- 主内存:所有线程共享,存储变量
- 工作内存:每个线程私有,存储变量的副本(缓存、寄存器)
JMM 核心三大特性:
| 特性 | 说明 | 保证手段 |
|---|---|---|
| 原子性 | 一个或多个操作要么全部执行且不被中断,要么全不执行 | synchronized、Lock、原子类 |
| 可见性 | 一个线程对共享变量的修改,其他线程能够立即看到 | volatile、synchronized、Lock |
| 有序性 | 禁止指令重排序,保证代码执行顺序与程序顺序一致 | volatile、happens-before 规则 |
Happens-Before 规则(JSR-133):
- 程序次序规则:一个线程内,书写在前的代码 happens-before 后面的代码
- volatile 规则:对 volatile 变量的写 happens-before 后续对该变量的读
- 锁规则:解锁 happens-before 后续的加锁
- 传递性:A happens-before B,B happens-before C ⇒ A happens-before C
源码(Unsafe 内存屏障):
// JVM 底层插入内存屏障
public native void loadFence(); // LoadLoad
public native void storeFence(); // StoreStore
public native void fullFence(); // StoreLoad
Q4:线程的同步有哪些方式?
答案
| 方式 | 核心原理 | 适用场景 |
|---|---|---|
synchronized | JVM 内置监视器锁(Monitor),自动加锁/释放 | 简单同步,方法或代码块级别 |
ReentrantLock | AQS(AbstractQueuedSynchronizer),手动 lock/unlock | 需要可中断、超时、公平锁的复杂场景 |
volatile | 内存屏障,保证可见性和有序性,不保证原子性 | 布尔标志位、状态变量 |
原子类(AtomicInteger) | CAS(Compare And Swap)无锁机制 | 计数器、累加器 |
ThreadLocal | 每个线程独立副本,空间换时间 | 避免共享(如 SimpleDateFormat) |
读写锁(ReadWriteLock) | 读共享、写互斥 | 读多写少场景 |
信号量(Semaphore) | 控制并发数量 | 限流、连接池 |
Q5:synchronized、ReentrantLock、volatile 的底层原理与比较
答案
synchronized 底层原理
- 对象头(Mark Word)存储锁信息(偏向锁、轻量级锁、重量级锁标识)
- 编译为
monitorenter/monitorexit指令 - 依赖操作系统的 mutex(重量级锁时)
// 源码对应字节码
public void syncMethod() {
synchronized (this) { count++; }
}
// 字节码:monitorenter → count++ → monitorexit
ReentrantLock 底层原理
- 基于 AQS(AbstractQueuedSynchronizer)
- 内部维护
volatile int state和 CLH 等待队列 - CAS 实现快速加锁
// ReentrantLock 的非公平锁实现
final void lock() {
if (compareAndSetState(0, 1)) // CAS 尝试获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 失败则入队阻塞
}
volatile 底层原理
- 写入 volatile 变量时,汇编层面增加
lock前缀指令 - 触发 缓存一致性协议(MESI),强制将当前缓存行写回主内存,并让其他 CPU 缓存行失效
- 禁止重排序:在 volatile 写前后插入内存屏障
三者对比表
| 特性 | synchronized | ReentrantLock | volatile |
|---|---|---|---|
| 实现层级 | JVM 内置 | Java API(AQS) | JVM + CPU |
| 锁性质 | 可重入、非公平(默认) | 可重入、公平/非公平可选 | 无锁 |
| 原子性保证 | ✅ | ✅ | ❌ |
| 可见性保证 | ✅ | ✅ | ✅ |
| 有序性保证 | ✅(代码块内) | ✅ | ✅(禁止重排) |
| 能否响应中断 | ❌ | ✅(lockInterruptibly()) | N/A |
| 超时尝试 | ❌ | ✅(tryLock(timeout)) | N/A |
| 条件变量 | wait/notify | Condition(可多个) | N/A |
| 性能(现代 JVM) | 优化后相近 | 相近 | 非常轻量 |
Q6:CAS 机制、偏向锁、锁升级机制(重点)
答案
CAS(Compare And Swap)
- 无锁原子操作:比较当前值与期望值,相等则更新为新值,否则失败
- 由 CPU 原子指令(如 x86 的
cmpxchg)保证 - Java 通过
sun.misc.Unsafe类提供
// AtomicInteger.incrementAndGet() 源码
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// unsafe 实现循环 CAS
do {
old = getIntVolatile(object, offset);
} while (!compareAndSwapInt(object, offset, old, old + delta));
CAS 的问题:
- ABA 问题:值从 A→B→A,CAS 会误认为未改变。解决:
AtomicStampedReference(带版本号) - 自旋开销:高并发下大量失败重试消耗 CPU
锁升级机制(synchronized 优化)
JVM 为了减少锁竞争开销,对 synchronized 做了优化:
| 锁状态 | 触发条件 | 原理 | 优势 |
|---|---|---|---|
| 无锁 | 对象刚创建 | Mark Word 为普通 hashCode | 无竞争 |
| 偏向锁 | 只有一个线程访问同步块 | Mark Word 存储线程 ID,每次进入无需 CAS | 消除无竞争时的同步开销 |
| 轻量级锁 | 第二个线程开始竞争(但未自旋成功) | 栈帧中创建 Lock Record,CAS 替换 Mark Word,自旋等待 | 避免操作系统内核态切换 |
| 重量级锁 | 自旋超过阈值(默认 10 次)或竞争激烈 | 升级为操作系统的 mutex,线程阻塞 | 保证公平但开销大 |
锁升级流程图:
flowchart TD
A[无锁状态] -->|第一个线程进入| B[偏向锁<br/>Mark Word存线程ID]
B -->|其他线程尝试获取锁| C[偏向锁撤销<br/>升级为轻量级锁]
C -->|自旋竞争锁| D{自旋成功?}
D -->|是| E[轻量级锁,继续持有]
D -->|自旋失败/次数超| F[膨胀为重量级锁<br/>OS mutex]
源码位置:hotspot/src/share/vm/runtime/synchronizer.cpp 中的 ObjectSynchronizer::slow_enter
Q7:死锁的产生条件与避免方法
答案
死锁的四个必要条件
- 互斥:资源一次只能被一个线程占用
- 持有并等待:线程持有至少一个资源,同时等待其他资源
- 不可剥夺:资源只能由持有者主动释放
- 循环等待:线程之间形成循环等待链
死锁示例:
Object lockA = new Object();
Object lockB = new Object();
// 线程 1
new Thread(() -> {
synchronized (lockA) {
sleep(100);
synchronized (lockB) { /* 操作 */ }
}
}).start();
// 线程 2
new Thread(() -> {
synchronized (lockB) {
sleep(100);
synchronized (lockA) { /* 操作 */ }
}
}).start();
死锁流程图:
flowchart LR
subgraph 死锁
T1[线程1] -- 持有锁A --> A[锁A]
T1 -- 等待锁B --> B[锁B]
T2[线程2] -- 持有锁B --> B
T2 -- 等待锁A --> A
end
避免死锁的方法
| 方法 | 说明 |
|---|---|
| 统一加锁顺序 | 所有线程按相同的顺序获取锁(先 lockA 后 lockB) |
| 使用超时锁 | tryLock(timeout),超时后释放已持有的锁 |
| 降低锁粒度 | 减少持有锁的时间,避免嵌套锁 |
| 使用无锁结构 | 原子类、ConcurrentHashMap 等 |
| 死锁检测 | JVM 工具 jstack 可检测死锁,或使用 ThreadMXBean 编程检测 |
使用 tryLock 避免死锁:
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
while (true) {
if (lock1.tryLock()) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try { /* work */ } finally { lock2.unlock(); }
break;
}
} finally { lock1.unlock(); }
}
}
Q8:线程的状态、如何终止、创建形式与返回值
答案
线程的 6 种状态
| 状态 | 说明 | 进入方式 | 退出方式 |
|---|---|---|---|
| NEW | 线程创建但未启动 | new Thread() | start() |
| RUNNABLE | 就绪或运行中 | start() 后 | 时间片用完或主动让出 |
| BLOCKED | 阻塞等待锁 | 进入 synchronized 块且锁被占用 | 获取到锁 |
| WAITING | 无限期等待 | wait()/join()/LockSupport.park() | notify()/notifyAll()/unpark() |
| TIMED_WAITING | 限期等待 | sleep(time)/wait(time)/join(time) | 超时或被唤醒 |
| TERMINATED | 已终止 | run() 结束或异常 | - |
状态转换图:
stateDiagram-v2
[*] --> NEW : new Thread()
NEW --> RUNNABLE : start()
RUNNABLE --> BLOCKED : 等待synchronized锁
RUNNABLE --> WAITING : wait()/join()
RUNNABLE --> TIMED_WAITING : sleep(time)/wait(time)
BLOCKED --> RUNNABLE : 获取到锁
WAITING --> RUNNABLE : notify()/notifyAll()
TIMED_WAITING --> RUNNABLE : 超时或被唤醒
RUNNABLE --> TERMINATED : run()结束/异常
TERMINATED --> [*]
线程如何终止(正确方式)
禁止使用 Thread.stop()(强制释放锁导致数据不一致)。推荐使用中断标志:
// 子线程任务
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
}
// 主线程中发起中断
workerThread.interrupt();
处理 InterruptedException:捕获后可以重新设置中断标志或直接退出。
线程的创建形式及返回值
| 形式 | 有无返回值 | 异常抛出 | 使用方式 |
|---|---|---|---|
Thread 子类 | 无(run() void) | 不支持 | new MyThread().start() |
Runnable | 无 | 不支持 | new Thread(runnable).start() |
Callable | 有(call() 返回 V) | 支持 | FutureTask + Thread 或线程池 |
获取返回值的示例:
Callable<String> callable = () -> "result";
FutureTask<String> task = new FutureTask<>(callable);
new Thread(task).start();
String result = task.get(); // 阻塞直到完成
Q9:wait()、notify()、notifyAll() 的作用?与 sleep() 的区别?
答案
wait()、notify()、notifyAll() 是 Object 类的 native 方法,用于线程间协作,必须在同步块(synchronized)内调用。
| 方法 | 作用 |
|---|---|
wait() | 释放当前对象的锁,线程进入 WAITING 状态,等待被 notify/notifyAll 唤醒 |
wait(timeout) | 带超时的等待,超时后自动唤醒 |
notify() | 随机唤醒一个在该对象上等待的线程 |
notifyAll() | 唤醒所有在该对象上等待的线程 |
sleep() vs wait() 对比
| 对比项 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread 静态方法 | Object 实例方法 |
| 是否释放锁 | ❌ 不释放 | ✅ 释放 |
| 是否需要同步块 | ❌ 不需要 | ✅ 必须(否则抛 IllegalMonitorStateException) |
| 唤醒方式 | 时间到或 interrupt() | notify/notifyAll/超时 |
| 适用场景 | 线程暂停一段时间 | 线程间通信(生产者-消费者) |
示例(生产者-消费者简化):
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait(); // 释放锁,等待数据
}
Object item = queue.poll();
}
Q10:进程与线程的核心区别?线程、线程池、协程的区别?为什么协程是最优解?
答案
进程 vs 线程
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 系统资源分配的基本单位 | 不独立拥有资源,共享进程资源 |
| 地址空间 | 独立的地址空间 | 共享进程地址空间 |
| 切换开销 | 大(涉及地址空间切换、刷新 TLB) | 小(仅保存/恢复 PC、寄存器) |
| 通信方式 | IPC(Binder、Socket、共享内存等) | 直接读写共享变量(需同步) |
| 健壮性 | 进程崩溃不影响其他进程 | 线程崩溃导致整个进程崩溃 |
线程 vs 线程池 vs 协程
| 对比项 | 线程(Thread) | 线程池(ThreadPool) | 协程(Coroutine) |
|---|---|---|---|
| 创建开销 | 高(~1MB 栈内存) | 复用线程,减少创建 | 极低(~几 KB 栈) |
| 调度方式 | 操作系统内核调度 | 内核调度 | 用户态调度(程序控制) |
| 并发数量 | 受限(几千) | 受限(线程池大小) | 可达数十万 |
| 阻塞影响 | 阻塞时线程挂起,浪费资源 | 同线程 | 挂起不阻塞线程,可继续执行其他协程 |
| 取消机制 | interrupt() 较麻烦 | 同线程 | 结构化并发,自动取消 |
| 生命周期管理 | 手动 | 手动关闭 | 自动绑定作用域(如 lifecycleScope) |
为什么协程是最优解(在 Android 中)?
- 轻量:一个线程可运行成千上万个协程,避免 OOM
- 非阻塞挂起:遇到 IO 等待时自动挂起,释放底层线程去执行其他协程,提高吞吐量
- 结构化并发:父协程取消时自动取消所有子协程,避免内存泄漏
- 简洁异步代码:用同步风格写异步逻辑(
suspend函数),告别回调地狱 - 与 LiveData/Flow 无缝集成:响应式编程友好
示例:
lifecycleScope.launch(Dispatchers.IO) {
val data = fetchData() // 挂起函数,不阻塞线程
withContext(Dispatchers.Main) {
textView.text = data
}
}
二、线程池核心原理篇
Q11:ThreadPoolExecutor 的 7 个核心参数及工作流程
答案
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(常驻存活)
int maximumPoolSize, // 最大线程数(核心 + 临时)
long keepAliveTime, // 临时线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务阻塞队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
| 参数 | 含义 |
|---|---|
corePoolSize | 核心线程数,即使空闲也不会被回收(除非设置 allowCoreThreadTimeOut(true)) |
maximumPoolSize | 最大线程数 = 核心线程 + 非核心线程 |
keepAliveTime | 非核心线程空闲存活时间,超过则回收 |
unit | 存活时间单位(TimeUnit.SECONDS 等) |
workQueue | 任务阻塞队列,用于暂存等待执行的任务 |
threadFactory | 线程工厂,可自定义线程名称、优先级、是否为守护线程 |
handler | 拒绝策略,当队列满且线程数达最大时触发 |
队列选择:
| 队列类型 | 特点 | 适用 |
|---|---|---|
ArrayBlockingQueue | 有界,FIFO | 控制资源使用 |
LinkedBlockingQueue | 可选有界/无界,吞吐量高 | 默认无界(注意 OOM) |
SynchronousQueue | 容量为 0,必须立即有线程处理 | CachedThreadPool |
PriorityBlockingQueue | 优先级队列,无界 | 任务分优先级 |
工作流程图:
flowchart TD
A[提交任务] --> B{当前线程数<br/>< corePoolSize?}
B -->|是| C[创建核心线程<br/>执行任务]
B -->|否| D{任务入队成功?}
D -->|是| E[等待核心线程空闲<br/>从队列取任务执行]
D -->|否| F{当前线程数<br/>< maximumPoolSize?}
F -->|是| G[创建非核心线程<br/>执行任务]
F -->|否| H[触发拒绝策略]
源码分析(execute() 方法):
// java.util.concurrent.ThreadPoolExecutor#execute
public void execute(Runnable command) {
if (command == null) throw new NullPointerException();
int c = ctl.get();
// 1. 当前线程数 < corePoolSize → 创建核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2. 任务入队
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false); // 创建非核心线程处理队列
}
// 3. 队列已满 → 创建非核心线程
else if (!addWorker(command, false))
reject(command); // 队列满且线程已达最大 → 拒绝
}
Q12:为什么 Executors 工具类创建的线程池存在风险?(生产环境禁止使用)
答案
Executors 的四种工厂方法都存在隐患:
| 方法 | 默认配置 | 隐患 |
|---|---|---|
newFixedThreadPool(n) | LinkedBlockingQueue 无界队列 | 任务堆积可能 OOM |
newSingleThreadExecutor() | LinkedBlockingQueue 无界队列 | 同上 |
newCachedThreadPool() | maxPoolSize=Integer.MAX_VALUE | 突发大量任务创建海量线程,CPU 过载 |
newScheduledThreadPool(n) | DelayedWorkQueue 无界队列 | 任务堆积可能 OOM |
正确做法:直接使用 ThreadPoolExecutor 构造函数,根据业务场景配置有界队列和合理的线程数。
Q13:CPU 密集型 vs IO 密集型任务的线程池参数如何设计?
答案
| 任务类型 | 特征 | 核心线程数建议 | 队列选择 | 典型场景 |
|---|---|---|---|---|
| CPU 密集型 | 大量计算,CPU 占用高 | CPU核数 + 1 | ArrayBlockingQueue(有界) | 图像处理、加密、复杂运算 |
| IO 密集型 | 大量等待(网络/磁盘) | CPU核数 × 2 或更大 | SynchronousQueue 或 LinkedBlockingQueue | 网络请求、文件读写、数据库操作 |
| 混合型 | 部分计算 + 部分等待 | 根据比例估算 | 有界队列 | 大多数业务逻辑 |
公式推导(IO 密集型):
最佳线程数 = CPU核数 / (1 - 阻塞系数)
阻塞系数 = IO等待时间 / (CPU计算时间 + IO等待时间)
例如:阻塞系数 0.9(90% 时间在等待),4 核 → 4 / (1-0.9) = 40 线程。
Android 设备实际配置建议:
int cpuCores = Runtime.getRuntime().availableProcessors();
// CPU 密集型(图片压缩)
ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(
cpuCores + 1, cpuCores + 1, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(20),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// IO 密集型(网络请求)
ThreadPoolExecutor ioPool = new ThreadPoolExecutor(
cpuCores * 2, cpuCores * 4, 30L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadPoolExecutor.DiscardPolicy()
);
Q14:线程池的 4 种拒绝策略及其适用场景
答案
| 策略 | 行为 | 适用场景 |
|---|---|---|
AbortPolicy(默认) | 直接抛出 RejectedExecutionException | 严格要求任务不丢失,失败需立即感知 |
CallerRunsPolicy | 由调用者线程执行任务 | 降低任务提交速度,防止雪崩 |
DiscardPolicy | 静默丢弃新任务 | 非关键任务,可接受丢失 |
DiscardOldestPolicy | 丢弃队列头部的任务,重试提交新任务 | 实时性要求高的场景(如 IM 消息) |
自定义拒绝策略示例:
RejectedExecutionHandler customHandler = (r, executor) -> {
Log.e("ThreadPool", "Task rejected, queue size: " + executor.getQueue().size());
saveToLocalStorage(r); // 降级:存入本地数据库稍后重试
};
Q15:线程池的状态及其转换(面试进阶题)
答案
ThreadPoolExecutor 使用一个 AtomicInteger ctl 同时存储运行状态(高 3 位)和线程数(低 29 位):
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; // 29
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 5 种状态(高 3 位)
private static final int RUNNING = -1 << COUNT_BITS; // 接受新任务并处理队列
private static final int SHUTDOWN = 0 << COUNT_BITS; // 不接受新任务,处理队列
private static final int STOP = 1 << COUNT_BITS; // 不接受新任务,不处理队列,中断执行中的线程
private static final int TIDYING = 2 << COUNT_BITS; // 所有任务终止,workerCount=0
private static final int TERMINATED = 3 << COUNT_BITS; // terminated() 执行完成
| 状态 | 高3位值 | 描述 | 是否接受新任务 | 是否处理队列任务 |
|---|---|---|---|---|
| RUNNING | -1 | 正常运行 | ✅ | ✅ |
| SHUTDOWN | 0 | 调用了 shutdown() | ❌ | ✅(处理完队列中的任务) |
| STOP | 1 | 调用了 shutdownNow() | ❌ | ❌(中断正在执行的任务) |
| TIDYING | 2 | 所有任务已终止,workerCount=0 | ❌ | ❌ |
| TERMINATED | 3 | terminated() 钩子方法执行完毕 | ❌ | ❌ |
状态转换图:
flowchart LR
RUNNING -->|shutdown| SHUTDOWN
RUNNING -->|shutdownNow| STOP
SHUTDOWN -->|队列为空且所有任务完成| TIDYING
STOP -->|所有工作线程退出| TIDYING
TIDYING -->|terminated执行完成| TERMINATED
Q16:线程池如何实现任务优先级排序?实战场景(直播弹幕 > 日志上报)
答案
使用 PriorityBlockingQueue 作为任务队列,队列根据任务的优先级排序。
实现步骤
- 定义优先级接口:
public interface PriorityRunnable extends Runnable {
int getPriority(); // 数值越小优先级越高
}
- 创建优先级队列:
BlockingQueue<Runnable> queue = new PriorityBlockingQueue<>(11,
(r1, r2) -> {
int p1 = (r1 instanceof PriorityRunnable) ? ((PriorityRunnable) r1).getPriority() : Integer.MAX_VALUE;
int p2 = (r2 instanceof PriorityRunnable) ? ((PriorityRunnable) r2).getPriority() : Integer.MAX_VALUE;
return Integer.compare(p1, p2);
}
);
- 创建线程池:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 30L, TimeUnit.SECONDS, queue
);
- 提交带优先级的任务:
// 高优先级:弹幕渲染(priority=1)
executor.execute(() -> renderDanmaku());
// 低优先级:日志上报(priority=10)
executor.execute(() -> uploadLog());
直播场景实战
- 弹幕渲染任务:必须快速处理,否则用户感觉卡顿 →
priority = 1 - 日志上报任务:可以延迟,不影响体验 →
priority = 10 - 还可结合 优先级动态调整:当队列长度超过阈值时,自动提升某些任务的优先级。
注意事项:
PriorityBlockingQueue是 无界队列,必须配合其他措施防止 OOM- 优先级相同的任务不保证 FIFO,可结合
AtomicLong添加序列号实现二次排序
流程图:
提交高优先级弹幕任务 -> 入队 -> 排在队头
提交低优先级日志任务 -> 入队 -> 排在队尾
Worker线程从队列take() -> 获取队头(高优先级) -> 执行
Q17:线程池如何调优,如何优化?
答案
调优步骤
- 确定任务类型:CPU 密集型还是 IO 密集型?决定
corePoolSize和队列 - 设置合理的队列容量:无界队列可能导致 OOM,有界队列需要根据任务峰值设置
- 选择合适的拒绝策略:
- 关键任务 →
AbortPolicy(抛出异常,上层重试) - 允许降级 →
CallerRunsPolicy(调用者线程执行,减缓提交速度) - 丢弃非关键任务 →
DiscardPolicy/DiscardOldestPolicy
- 关键任务 →
- 监控线程池状态:活跃线程数、队列长度、拒绝次数
- 动态调整参数(高级):
setCorePoolSize()、setMaximumPoolSize()可以在运行时修改 - 线程命名:通过
ThreadFactory给线程设置有意义的名称,便于jstack调试 - 避免任务泄漏:确保
Runnable不会因为异常导致线程意外终止
优化技巧
// 动态调整核心线程数(根据当前负载)
public void adjustPoolSize(int targetCoreSize) {
int current = executor.getCorePoolSize();
if (targetCoreSize > current) {
executor.setCorePoolSize(targetCoreSize);
executor.setMaximumPoolSize(targetCoreSize * 2);
} else if (targetCoreSize < current) {
executor.setCorePoolSize(targetCoreSize);
executor.setMaximumPoolSize(targetCoreSize * 2);
}
}
// 线程命名工厂
ThreadFactory namedFactory = new ThreadFactoryBuilder()
.setNameFormat("MyPool-%d")
.setDaemon(false)
.setPriority(Thread.NORM_PRIORITY)
.build();
调优闭环流程图:
flowchart LR
A[压测] --> B[监控指标]
B --> C{CPU/队列深度/拒绝次数}
C -->|异常| D[调整参数]
D --> A
C -->|正常| E[保持配置]
三、Android 特有线程机制篇
Q18:Binder 线程池的工作原理(高级 Framework 题)
答案
Binder 线程池是 Android Framework 层面实现跨进程通信的核心机制,与传统 Java 线程池设计完全不同。
核心特点
- 非传统线程池结构:每个进程只有一个
PoolThread类继承自 Thread - 线程创建和管理由 Binder Driver 控制,而非应用层
- 主线程 + 非主线程:每个 binder 线程池只有一个主线程,启动时创建;非主线程由 binder driver 按需创建
- 默认大小:16(1 个主线程 + 15 个非主线程),可通过
ProcessState::setMaxThreads()修改
源码分析(C++ 层)
// frameworks/native/libs/binder/ProcessState.cpp
void ProcessState::startThreadPool() {
AutoMutex _l(mLock);
if (!mThreadPoolStarted) {
mThreadPoolStarted = true;
spawnPooledThread(true); // 创建主线程,isMain=true
}
}
void ProcessState::spawnPooledThread(bool isMain) {
if (mThreadPoolStarted) {
String8 name = makeBinderThreadName(); // 格式 "Binder:x"
sp<Thread> t = new PoolThread(isMain);
t->run(name.string());
}
}
// PoolThread 的 run 最终会调用 IPCThreadState::joinThreadPool
void IPCThreadState::joinThreadPool(bool isMain) {
// 进入循环,从 Binder Driver 读取命令并处理
do {
result = talkWithDriver(); // 阻塞等待请求
executeCommand(cmd); // 执行跨进程调用
} while (result != -ECONNREFUSED && result != -EBADF);
}
非主线程的创建时机
- Client 进程向 Binder Driver 发送 IPC 请求时
- 当 Client 进程发起 IPC 请求,且 Server 进程当前所有 Binder 线程都处于忙碌状态时,Binder Driver 会通知 Server 进程 新创建一条 Binder 线程(除非已达到最大线程数)
验证 Binder 线程数
# adb shell 查看进程的 binder 线程
ps -T | grep binder | grep <your_pid>
# 输出类似:
# u0_a123 12345 12350 binder:12345_1
# u0_a123 12345 12351 binder:12345_2
应用层注意事项
- 普通应用开发者通常不需要直接操作 Binder 线程池,但需要知道:大量同步 Binder 调用可能导致 Binder 线程耗尽,引发 ANR
- 避免在主线程做长时间的 Binder 调用(如跨进程查询数据库)
Binder IPC 流程图:
sequenceDiagram
participant Client as Client进程
participant Driver as Binder Driver
participant Server as Server进程
participant Pool as Binder线程池
Client->>Driver: 发起IPC请求
Driver->>Pool: 分配空闲线程
Pool->>Server: 执行onTransact()
Server-->>Pool: 返回结果
Pool-->>Driver: 传递结果
Driver-->>Client: 返回结果
四、线程同步与安全篇
Q19:ThreadLocal 的原理及内存泄漏问题
答案
原理
每个 Thread 内部维护一个 ThreadLocalMap,以 ThreadLocal 的弱引用为 key,存储线程隔离的值。
// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
// ThreadLocal.java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) return (T) e.value;
}
return setInitialValue();
}
ThreadLocalMap.Entry 的弱引用设计:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key 是弱引用
value = v;
}
}
内存泄漏问题分析
| 场景 | 是否泄漏 | 原因 |
|---|---|---|
| Thread 存活,ThreadLocal 被 GC | ✅ 泄漏 | value 被 Entry 强引用,无法回收 |
| Thread 销毁 | ❌ 不泄漏 | ThreadLocalMap 随之销毁 |
主动调用 remove() | ❌ 不泄漏 | 手动清除 Entry |
解决方案:使用后调用 remove()。
ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd")
);
try {
dateFormat.get().format(new Date());
} finally {
dateFormat.remove(); // 防止内存泄漏
}
应用场景
- 避免
SimpleDateFormat多线程共享 - 存储每个线程的 Looper(Handler 机制)
- 存储每个线程的数据库连接、Session 等
Q20:原子类(AtomicXXX)的实现原理(CAS + volatile)
答案
原子类(如 AtomicInteger)内部使用 volatile 保证可见性 + CAS 保证原子性。
public class AtomicInteger {
private volatile int value;
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
CAS 循环:
do {
old = this.get();
new = old + delta;
} while (!compareAndSwap(old, new));
常用原子类:
| 原子类 | 用途 |
|---|---|
AtomicInteger / AtomicLong | 整数/长整型原子操作 |
AtomicBoolean | 布尔值原子操作 |
AtomicReference<V> | 引用类型原子操作 |
AtomicIntegerArray | 整数数组原子操作 |
LongAdder | 高并发累加器,分段计数,性能优于 AtomicLong |
AtomicStampedReference | 带版本号的引用,解决 ABA 问题 |
适用场景:
- 计数器(请求计数、统计)
- 标识位(开关状态)
- 累加器(性能统计)
Q21:CountDownLatch、CyclicBarrier、Semaphore 的使用场景与原理
答案
| 同步器 | 原理 | 使用场景 |
|---|---|---|
CountDownLatch | 计数器递减,归零时释放等待线程 | 一个线程等待多个任务完成 |
CyclicBarrier | 计数器递减到零后重置,所有线程同时继续 | 多线程相互等待到齐 |
Semaphore | 许可证机制,控制同时访问数量 | 限流、连接池 |
CountDownLatch 示例
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
executor.execute(() -> {
doWork();
latch.countDown(); // 计数器减1
});
}
latch.await(); // 等待3个任务完成
CyclicBarrier 示例
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有人都到达,继续执行");
});
for (int i = 0; i < 3; i++) {
executor.execute(() -> {
doWork();
barrier.await(); // 等待其他线程
doNextWork();
});
}
Semaphore 示例
Semaphore semaphore = new Semaphore(10); // 最多10个并发
semaphore.acquire();
try {
doLimitedTask();
} finally {
semaphore.release();
}
流程图(CountDownLatch):
sequenceDiagram
participant Main as 主线程
participant T1 as 线程1
participant T2 as 线程2
participant T3 as 线程3
Main->>T1: 启动
Main->>T2: 启动
Main->>T3: 启动
Main->>Main: latch.await() 阻塞
T1->>T1: doWork()
T1->>Main: countDown()
T2->>T2: doWork()
T2->>Main: countDown()
T3->>T3: doWork()
T3->>Main: countDown()
Main->>Main: 被唤醒,继续执行
Q22:Future 和 CompletableFuture 的异步编程
答案
Future
- 表示异步计算结果
- 通过
get()阻塞获取结果 - 缺点:不能手动完成、无回调、不能链式组合
Future<String> future = executor.submit(() -> "result");
String result = future.get(); // 阻塞
CompletableFuture(Java 8+)
- 实现
Future和CompletionStage - 支持回调、组合、异常处理
CompletableFuture.supplyAsync(() -> fetchData())
.thenApply(data -> process(data))
.thenAccept(result -> updateUI(result))
.exceptionally(e -> { log(e); return null; });
CompletableFuture 常用方法:
| 方法 | 作用 |
|---|---|
supplyAsync() | 异步执行有返回值的任务 |
thenApply() | 转换结果 |
thenAccept() | 消费结果(无返回值) |
thenRun() | 执行 Runnable |
thenCompose() | 链式组合多个异步任务 |
thenCombine() | 组合两个独立任务的结果 |
allOf() | 等待所有任务完成 |
anyOf() | 等待任意一个任务完成 |
exceptionally() | 异常处理 |
在 Android 中:可与线程池结合,但注意生命周期(需手动取消)。
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
// 耗时操作
}, executor);
// Activity 销毁时取消
@Override
protected void onDestroy() {
future.cancel(true);
super.onDestroy();
}
五、高级实践与优化篇
Q23:Android 中如何监控线程池的状态?
答案
方法一:自定义 ThreadPoolExecutor 添加统计
public class MonitoredThreadPoolExecutor extends ThreadPoolExecutor {
private final AtomicLong totalTaskCount = new AtomicLong(0);
private final AtomicLong completedTaskCount = new AtomicLong(0);
private final AtomicLong rejectedTaskCount = new AtomicLong(0);
private final AtomicLong maxQueueSize = new AtomicLong(0);
public MonitoredThreadPoolExecutor(...) { super(...); }
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
// 记录任务开始时间
((MonitoredRunnable) r).setStartTime(System.currentTimeMillis());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
completedTaskCount.incrementAndGet();
long cost = System.currentTimeMillis() - ((MonitoredRunnable) r).getStartTime();
if (cost > 1000) {
Log.w("ThreadPool", "Slow task: " + cost + "ms");
}
}
@Override
public void execute(Runnable command) {
totalTaskCount.incrementAndGet();
super.execute(command);
long currentQueueSize = getQueue().size();
maxQueueSize.updateAndGet(old -> Math.max(old, currentQueueSize));
}
@Override
protected void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
rejectedTaskCount.incrementAndGet();
super.rejectedExecution(r, e);
}
public void logStatus() {
Log.d("ThreadPool", String.format(
"PoolSize=%d, Active=%d, Queue=%d, Completed=%d, Rejected=%d, MaxQueue=%d",
getPoolSize(), getActiveCount(), getQueue().size(),
completedTaskCount.get(), rejectedTaskCount.get(), maxQueueSize.get()
));
}
}
方法二:使用 Android Profiler
- CPU Profiler:实时查看线程数量、线程状态(运行/等待/阻塞)
- Memory Profiler:检测线程导致的内存泄漏
方法三:定期 dump 线程信息
adb shell kill -3 <pid> # 生成 traces.txt
或在代码中:
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long[] threadIds = threadBean.getAllThreadIds();
for (long id : threadIds) {
ThreadInfo info = threadBean.getThreadInfo(id, Integer.MAX_VALUE);
Log.d("ThreadDump", info.toString());
}
方法四:集成 LeakCanary
LeakCanary 可以检测 Activity 泄漏,间接发现因线程池任务持有 Activity 引用导致的泄漏。
方法五:使用 StrictMode
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build());
监控流程图:
flowchart LR
A[定时任务每30s] --> B[调用getActiveCount/getQueue.size]
B --> C[上报到监控平台]
C --> D{告警规则}
D -->|队列深度>1000| E[发送告警]
D -->|active>corePoolSize*1.5| E
Q24:如何避免使用线程池导致的内存泄漏?
答案
常见泄漏场景与解决方案
| 泄漏场景 | 解决方案 |
|---|---|
| 任务持有 Activity 引用 | 使用 WeakReference 或传入 ApplicationContext |
| 线程池未关闭 | Application 级别共享,无需关闭 |
| 匿名内部类任务 | 改为静态内部类 + 弱引用 |
| 延迟任务未取消 | 取消所有未执行任务 |
public class ImageLoader {
private static final ExecutorService sExecutor =
new MonitoredThreadPoolExecutor(...); // 单例
public void loadImage(String url, ImageView imageView) {
sExecutor.submit(new ImageLoadTask(url, new WeakReference<>(imageView)));
}
private static class ImageLoadTask implements Runnable {
private final String url;
private final WeakReference<ImageView> imageViewRef;
ImageLoadTask(String url, WeakReference<ImageView> ref) {
this.url = url;
this.imageViewRef = ref;
}
@Override
public void run() {
Bitmap bitmap = loadFromNetwork(url);
// 切换到主线程更新 UI
mainHandler.post(() -> {
ImageView iv = imageViewRef.get();
if (iv != null && iv.getContext() != null) {
iv.setImageBitmap(bitmap);
}
});
}
}
}
最佳实践
- Application 级别维护统一的线程池实例,避免重复创建
- 使用自定义线程池而非 Executors 工具类
- 结合 LeakCanary + Android Profiler 监控线程和内存状态
- 新项目优先使用 Kotlin 协程,与现有线程池可以混合使用
- 页面销毁时取消所有未完成的任务
// Activity 中取消任务
@Override
protected void onDestroy() {
for (Future<?> future : pendingTasks) {
future.cancel(true);
}
super.onDestroy();
}