10.02 线程与线程池
10.01 解决了"结果怎么切回主线程",本节解决另一半问题:工作线程怎么管。裸 new Thread() 有三大问题:创建销毁开销大、无限并发耗尽资源、无法统一管理。线程池(复用线程 + 控制并发 + 统一调度)是 Android 后台任务的标准答案——AsyncTask、Glide、OkHttp、Room 内部全是线程池。
# 一、Thread 基础回顾
// 两种基本用法
new Thread(new Runnable() {
@Override
public void run() {
String result = downloadData(); // 耗时操作
// 更新UI必须切回主线程(Handler机制,见10.01)
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
textView.setText(result);
}
});
}
}, "download-thread").start();
裸 Thread 的问题:
- 创建销毁开销大:每次任务都新建线程(内核调度 + 栈内存分配),高频小任务得不偿失
- 无法限流:列表 100 个 item 同时
new Thread加载图片 → 并发失控 → OOM 或设备发热 - 无统一管理:页面销毁后线程还在跑(结果回传给已销毁的 View)、没有取消/优先级/异常收集
线程池一次解决这三个问题。
# 二、ThreadPoolExecutor:核心参数与工作流程
Java/Android 的线程池本体是 ThreadPoolExecutor,七个构造参数:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(常驻)
int maximumPoolSize, // 最大线程数(核心+非核心上限)
long keepAliveTime, // 非核心线程闲置存活时间
TimeUnit unit, // 存活时间单位
BlockingQueue<Runnable> workQueue, // 任务排队队列
ThreadFactory threadFactory, // 线程工厂(命名、优先级)
RejectedExecutionHandler handler // 拒绝策略
)
# 提交任务的四步决策流程
一个反直觉的关键点:队列满之后才会扩容到 maximumPoolSize(先入队再扩容)。因此如果用无界队列(如默认的 LinkedBlockingQueue),队列永远不满,maximumPoolSize 永远不生效——这正是 Executors 预置线程池被诟病的根源(见第三节)。
# 常用阻塞队列与拒绝策略
workQueue 常见选择:
LinkedBlockingQueue:链表队列,默认无界(容量 Integer.MAX_VALUE),任务堆积风险ArrayBlockingQueue(n):定长数组队列,有界可控(推荐)SynchronousQueue:不存任务,来一个必须立刻有线程接走(配合CachedThreadPool)
拒绝策略(队列满 + 线程到 max 时):
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) | 抛出 RejectedExecutionException |
CallerRunsPolicy | 由提交任务的线程自己执行(天然限流降速) |
DiscardPolicy | 静默丢弃新任务 |
DiscardOldestPolicy | 丢弃队列最老的任务,腾位置给新任务 |
# 三、为什么不推荐 Executors 预置线程池
Executors 提供四种预置池,阿里 Java 开发手册(Android 同样适用)明确禁止使用:
| 预置池 | 配置 | 风险 |
|---|---|---|
newFixedThreadPool(n) | 核心n、队列无界 | 任务堆积 → OOM |
newCachedThreadPool(0, MAX) | 最大线程 Integer.MAX_VALUE | 线程无限创建 → OOM/线程耗尽 |
newSingleThreadExecutor() | 核心1、队列无界 | 同 FixedThreadPool 的堆积风险 |
newScheduledThreadPool(n) | 核心n、队列无界 | 同上 |
正确姿势:显式 new ThreadPoolExecutor,所有参数自己定:
/**
* 全局统一的线程池管理(单例).
*/
public class ThreadPoolManager {
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 经验公式:CPU密集型=核数+1;IO密集型=核数*2(下载/读写多属于此类)
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAX_POOL_SIZE = CPU_COUNT * 2 + 1;
private static class Holder {
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
30L, TimeUnit.SECONDS, // 非核心线程闲置30秒回收
new LinkedBlockingQueue<Runnable>(128), // 有界队列,防堆积OOM
new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
// 命名规范:方便线上排查时定位线程
Thread t = new Thread(r, "app-worker-" + mCount.getAndIncrement());
t.setPriority(Thread.NORM_PRIORITY - 1); // 略低于UI优先级
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy()); // 打满时让调用线程自己跑(降速不丢任务)
}
public static ThreadPoolExecutor getExecutor() {
return Holder.EXECUTOR;
}
public static void execute(Runnable task) {
getExecutor().execute(task);
}
}
// 使用示例:后台计算 + Handler回主线程(与10.01衔接)
ThreadPoolManager.execute(new Runnable() {
@Override
public void run() {
final String result = queryDatabase(); // 耗时IO
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
textView.setText(result);
}
});
}
});
# 四、线程池大小的经验法则
| 任务类型 | 经验公式 | 原因 |
|---|---|---|
| CPU 密集型(计算、编解码) | 核心数 + 1 | 线程多了只会增加上下文切换开销 |
| IO 密集型(网络、磁盘) | 核心数 × 2 | CPU 大部分时间在等 IO,可以多线程交错执行 |
实际项目中更推荐按业务域拆池(而非一个大池):网络请求池、磁盘 IO 池、图片解码池分开配置,避免互相挤兑。OkHttp/Glide 内部就是这么做的。
# 五、线程协作基础工具
线程池之外,并发编程还有几个基础工具(此处只列基础用法,深入属于 Java 并发专题):
// 1. 原子类:无锁的计数器(多线程统计)
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
// 2. volatile:可见性关键字(状态标志位)
private volatile boolean mCancelled = false; // 工作线程循环检查该标志
// 3. synchronized:方法/代码块互斥(如线程安全的单例)
public synchronized void addItem(Object item) { ... }
// 4. CountDownLatch:等待N个并行任务完成(如3个接口都返回后再刷新UI)
final CountDownLatch latch = new CountDownLatch(3);
ThreadPoolManager.execute(() -> { fetchUser(); latch.countDown(); });
ThreadPoolManager.execute(() -> { fetchFeed(); latch.countDown(); });
ThreadPoolManager.execute(() -> { fetchAds(); latch.countDown(); });
latch.await(); // 主线程等待(或用handler + 循环检查)
refreshUI();
# 六、AsyncTask 与 IntentService:废弃方案回顾
| 方案 | 现状 | 废弃原因 |
|---|---|---|
AsyncTask | API 30 起废弃 | 与生命周期绑定混乱(持有Activity泄漏)、池全局共享互相阻塞、结果回调易丢失 |
IntentService | API 30 起废弃 | 每次启动都新建 Service + HandlerThread,开销大;用 WorkManager 或线程池+Service 替代 |
只读旧代码时需要认识它们(核心结构:AsyncTask = 线程池 + Handler 切主线程;IntentService = Service + HandlerThread),新代码一律线程池(或后续的 WorkManager/协程)。
# 七、常见问题与陷阱
- 子线程 Toast/Dialog 崩溃:Toast 内部依赖 Looper——子线程要先
Looper.prepare(),或切回主线程再弹。 - Thread 未 start:
new Thread()只是创建对象,忘了.start()什么都不执行。 - 在子线程 sleep 模拟"主线程延迟":
Thread.sleep放在主线程会直接卡 UI 甚至 ANR;主线程延迟用postDelayed。 - 无界队列 + CallerRunsPolicy 的误解:队列无界时永远走不到拒绝策略,CallerRuns 形同虚设——两个参数要配套设计。
- 线程优先级:后台线程适当调低(
NORM_PRIORITY - 1),避免与主线程争抢 CPU 影响 UI 流畅度。 - 页面销毁后任务还在跑:结果回传时先判断 Activity 状态(弱引用判空,模式同 10.01 的 SafeHandler),或任务持有可取消的引用(
Future.cancel(true))。
# 结束
本节介绍了线程与线程池:裸 Thread 的三大问题、ThreadPoolExecutor 七参数与"先入队再扩容"的四步决策流程(配流程图)、四种拒绝策略、Executors 预置池被禁用的原因、手写线程池的最佳实践(按业务拆池、有界队列、命名工厂、CallerRuns 降速)、池大小经验公式(CPU 密集 N+1 / IO 密集 2N)、基础并发工具(原子类/volatile/synchronized/CountDownLatch),以及 AsyncTask/IntentService 废弃方案的回顾。至此"工作线程怎么管 + 结果怎么切回主线程"两个问题都有了标准答案,下一节看违反规则会发生什么:10.03 ANR 与主线程规则。更多内容请参考:后台线程 (opens new window)。