10.02 线程与线程池

2025/3/5 Android 开发基础

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 的问题:

  1. 创建销毁开销大:每次任务都新建线程(内核调度 + 栈内存分配),高频小任务得不偿失
  2. 无法限流:列表 100 个 item 同时 new Thread 加载图片 → 并发失控 → OOM 或设备发热
  3. 无统一管理:页面销毁后线程还在跑(结果回传给已销毁的 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/协程)。

# 七、常见问题与陷阱

  1. 子线程 Toast/Dialog 崩溃:Toast 内部依赖 Looper——子线程要先 Looper.prepare(),或切回主线程再弹。
  2. Thread 未 start:new Thread() 只是创建对象,忘了 .start() 什么都不执行。
  3. 在子线程 sleep 模拟"主线程延迟":Thread.sleep 放在主线程会直接卡 UI 甚至 ANR;主线程延迟用 postDelayed。
  4. 无界队列 + CallerRunsPolicy 的误解:队列无界时永远走不到拒绝策略,CallerRuns 形同虚设——两个参数要配套设计。
  5. 线程优先级:后台线程适当调低(NORM_PRIORITY - 1),避免与主线程争抢 CPU 影响 UI 流畅度。
  6. 页面销毁后任务还在跑:结果回传时先判断 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)。