Android 线程池 笔记

5 阅读12分钟

线程池是 Android 开发中管理并发任务的核心工具。合理使用线程池能有效控制资源消耗、提升响应速度,而优化不当则可能导致内存泄漏、线程爆炸或性能瓶颈。下面从原理到实践,全面讲解线程池及其优化策略。


一、线程池基础

1.1 为什么需要线程池

  • 降低资源开销:线程的创建和销毁代价高昂,线程池复用线程,减少开销。
  • 提高响应速度:任务到达时无需等待线程创建,直接执行。
  • 管理线程数量:避免无限制创建线程导致系统资源耗尽。
  • 提供统一管理:如定时执行、任务队列控制等。

1.2 核心参数(ThreadPoolExecutor

参数含义作用
corePoolSize核心线程数即使空闲也保留的线程数(除非设置了allowCoreThreadTimeOut
maximumPoolSize最大线程数线程池允许创建的最大线程数
keepAliveTime非核心线程空闲存活时间超过此时间且当前线程数>corePoolSize,多余线程会被回收
unit时间单位keepAliveTime的单位
workQueue任务队列存储等待执行的任务的阻塞队列
threadFactory线程工厂用于创建新线程,可设置线程名、优先级等
handler拒绝策略当队列满且线程数达到maximumPoolSize时,对新任务的处理方式

工作流程

  1. 当提交任务时,如果当前线程数 < corePoolSize,创建新线程执行。
  2. 如果当前线程数 >= corePoolSize,将任务加入 workQueue。
  3. 如果 workQueue 已满,且当前线程数 < maximumPoolSize,创建新线程执行。
  4. 如果 workQueue 已满且当前线程数 >= maximumPoolSize,执行拒绝策略。

1.3 Java 提供的拒绝策略

  • AbortPolicy(默认):直接抛出 RejectedExecutionException
  • CallerRunsPolicy:由提交任务的线程自己执行该任务。
  • DiscardPolicy:直接丢弃任务,不抛异常。
  • DiscardOldestPolicy:丢弃队列中最老的任务,然后重新提交新任务。

二、Android 中常用的线程池

Android 开发中通常通过 Executors 工厂类创建线程池,但这些默认实现可能存在隐患,需要根据场景选择或自定义。

线程池类型创建方式特点适用场景潜在问题
FixedThreadPoolExecutors.newFixedThreadPool(int nThreads)核心线程数 = 最大线程数,无超时,使用无界队列 LinkedBlockingQueue长期稳定任务,如后台同步无界队列可能导致任务堆积,内存溢出
CachedThreadPoolExecutors.newCachedThreadPool()核心线程数0,最大线程数Integer.MAX_VALUE,超时60秒,使用 SynchronousQueue大量短期任务,如网络请求线程数无限制,可能创建过多线程导致崩溃
ScheduledThreadPoolExecutors.newScheduledThreadPool(int corePoolSize)核心线程固定,无界队列 DelayedWorkQueue,支持定时/周期任务定时任务,如心跳检测无界队列,任务堆积风险
SingleThreadExecutorExecutors.newSingleThreadExecutor()核心线程数 = 最大线程数 = 1,无界队列需要顺序执行的任务,如文件写入无界队列,任务堆积

特别提醒Executors 的默认实现大多使用无界队列LinkedBlockingQueue 默认容量为 Integer.MAX_VALUE),当任务生产速度大于消费速度时,队列无限膨胀,可能导致内存溢出。因此建议显式创建 ThreadPoolExecutor 并指定有界队列


三、线程池优化策略

3.1 合理配置线程数

  • CPU密集型任务:线程数 ≈ CPU核心数 + 1,避免过多线程竞争CPU。
    int cpuCount = Runtime.getRuntime().availableProcessors();
    int corePoolSize = cpuCount + 1;
    
  • IO密集型任务:线程数可以较大,通常为 2 * CPU核心数。因为IO操作会阻塞线程,让出CPU。
  • 混合型任务:可根据任务等待时间与计算时间的比例估算,或通过压测调整。

3.2 选择合适的工作队列

  • 有界队列:如 ArrayBlockingQueueLinkedBlockingQueue 指定容量,防止任务堆积。
  • 同步移交队列SynchronousQueue 不存储任务,直接交给线程执行,适合 CachedThreadPool
  • 优先级队列PriorityBlockingQueue 可实现任务优先级,但需注意公平性。

示例:创建一个 CPU 密集型任务的线程池

int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
int maxPoolSize = corePoolSize; // CPU密集型一般不再增加
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(128); // 有界队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize, maxPoolSize,
        60L, TimeUnit.SECONDS,
        queue,
        new ThreadFactoryBuilder().setNameFormat("cpu-pool-%d").build(),
        new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

3.3 自定义线程工厂

默认线程工厂创建的线程名称为 pool-x-thread-y,不利于问题定位。建议自定义 ThreadFactory,设置有意义的名称和后台属性。

public class NamedThreadFactory implements ThreadFactory {
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    public NamedThreadFactory(String namePrefix) {
        this.namePrefix = namePrefix;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement());
        t.setDaemon(false); // 根据需要设置
        t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

3.4 拒绝策略选择

  • CallerRunsPolicy:适合对任务丢失敏感的场景,由调用者线程执行,可减缓任务提交速度。
  • 自定义策略:例如记录日志、降级处理或发送到备用线程池。
RejectedExecutionHandler handler = (r, executor) -> {
    Log.w("ThreadPool", "Task rejected: " + r.toString());
    // 可以尝试重试或加入备用队列
};

3.5 监控线程池状态

定期输出线程池指标,帮助调优:

  • getPoolSize():当前线程数
  • getActiveCount():活跃线程数
  • getQueue().size():队列大小
  • getCompletedTaskCount():已完成任务数
  • getTaskCount():总任务数
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
    Log.d("ThreadPool", String.format("PoolSize: %d, Active: %d, Queue: %d, Completed: %d",
            executor.getPoolSize(),
            executor.getActiveCount(),
            executor.getQueue().size(),
            executor.getCompletedTaskCount()));
}, 0, 10, TimeUnit.SECONDS);

3.6 避免内存泄漏

  • 确保提交的任务不再持有外部引用(如Activity),避免无法释放。
  • 在组件销毁时,正确关闭线程池:
    executor.shutdown(); // 不再接受新任务,等待已有任务完成
    try {
        if (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
            executor.shutdownNow(); // 强制停止
        }
    } catch (InterruptedException e) {
        executor.shutdownNow();
    }
    

3.7 使用 Kotlin 协程作为现代替代方案

在 Kotlin 项目中,协程是更轻量、更安全的并发方案。它通过调度器管理线程池:

  • Dispatchers.IO:用于 IO 密集型任务,线程池可按需扩张。
  • Dispatchers.Default:用于 CPU 密集型任务,线程数等于 CPU 核心数。
  • Dispatchers.Main:主线程。

协程避免了直接操作线程池的复杂性,并支持结构化并发,自动取消任务。


四、实际案例分析

案例1:图片加载库的线程池优化

Glide 内部使用自定义线程池,核心线程数根据 CPU 核心数调整,并限制最大线程数。它使用 PriorityBlockingQueue 实现优先级(如缩略图请求优先级低)。

案例2:网络请求线程池

网络请求通常为 IO 密集型,可用 CachedThreadPool 配合 SynchronousQueue,但为防止线程无限增长,可限制最大线程数:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
        0, 64, // 最大限制64个线程
        60L, TimeUnit.SECONDS,
        new SynchronousQueue<>(),
        new NamedThreadFactory("network")
);

案例3:防止队列堆积导致 OOM

某 App 后台任务队列使用无界队列,某次突发任务导致队列积压数百万个任务,最终内存溢出。优化方案:改用有界队列,并设置拒绝策略为 CallerRunsPolicy,让UI线程承担部分任务,间接反馈调节生产速度。


五、总结

线程池优化没有“银弹”,需根据任务特性(CPU/IO密集、优先级、量级)和系统资源动态调整。核心要点:

  • 明确参数含义,避免使用 Executors 默认无界队列。
  • 监控指标,通过数据指导调优。
  • 适配业务,选择合适的拒绝策略和线程数。
  • 结合协程,简化并发管理。

线程池监控是确保应用稳定性和性能的关键环节。通过监控,可以及时发现线程池的异常行为(如任务堆积、线程泄露、拒绝任务等),并为参数调优提供数据支持。下面从监控指标、实现方式、数据分析与优化等方面详细讲解。


线程池监控

一、为什么需要线程池监控?

  • 防止资源耗尽:线程池可能因任务积压导致内存溢出,或因线程数失控导致系统负载过高。
  • 定位性能瓶颈:任务执行时间过长、频繁等待等现象可通过监控发现。
  • 排查问题:当出现卡顿或崩溃时,监控数据可以帮助判断是否与线程池相关。
  • 动态调优:根据实际负载调整线程池参数,提升资源利用率。

二、核心监控指标

指标含义获取方式作用
当前线程数getPoolSize()线程池现有线程总数(包括核心和非核心)判断线程是否被过度创建
活跃线程数getActiveCount()正在执行任务的线程数反映当前并行度
核心线程数getCorePoolSize()配置的核心线程数检查配置是否合理
最大线程数getMaximumPoolSize()配置的最大线程数检查是否达到上限
队列大小getQueue().size()等待队列中的任务数判断任务积压程度
队列剩余容量getQueue().remainingCapacity()队列剩余容量判断队列是否即将满
已完成任务数getCompletedTaskCount()已执行完成的任务总数衡量吞吐量
总任务数getTaskCount()已提交任务总数(近似值)结合完成数计算积压
拒绝任务数自定义通过自定义拒绝策略统计判断系统是否过载
任务执行耗时自定义包装任务,记录开始和结束时间定位慢任务
线程创建总数自定义通过自定义线程工厂统计监测线程泄露

三、监控实现方式

3.1 定期采集线程池状态

最简单的方式是使用一个定时任务(如 ScheduledExecutorService)周期性地获取线程池的各项指标并输出或上报。

public class ThreadPoolMonitor {
    private final ThreadPoolExecutor executor;
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

    public ThreadPoolMonitor(ThreadPoolExecutor executor) {
        this.executor = executor;
    }

    public void startMonitoring(long period, TimeUnit unit) {
        scheduler.scheduleAtFixedRate(() -> {
            int poolSize = executor.getPoolSize();
            int activeCount = executor.getActiveCount();
            long completedCount = executor.getCompletedTaskCount();
            long taskCount = executor.getTaskCount();
            int queueSize = executor.getQueue().size();

            Log.d("ThreadPoolMonitor", 
                String.format("poolSize=%d, active=%d, completed=%d, taskCount=%d, queueSize=%d",
                poolSize, activeCount, completedCount, taskCount, queueSize));

            // 可添加报警逻辑:如果queueSize > threshold,则告警
        }, 0, period, unit);
    }

    public void stopMonitoring() {
        scheduler.shutdown();
    }
}

使用

ThreadPoolExecutor executor = new ThreadPoolExecutor(...);
ThreadPoolMonitor monitor = new ThreadPoolMonitor(executor);
monitor.startMonitoring(10, TimeUnit.SECONDS);

3.2 任务耗时监控

通过包装 Runnable 或重写 beforeExecute / afterExecute 来统计每个任务的执行时间。

public class MonitoredThreadPoolExecutor extends ThreadPoolExecutor {
    public MonitoredThreadPoolExecutor(...) { super(...); }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        // 记录开始时间,可用 ThreadLocal 存储
        ThreadLocalHolder.startTime.set(System.currentTimeMillis());
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        Long start = ThreadLocalHolder.startTime.get();
        if (start != null) {
            long cost = System.currentTimeMillis() - start;
            if (cost > SLOW_THRESHOLD) {
                Log.w("ThreadPool", "Slow task: " + r + " cost " + cost + "ms");
            }
        }
        ThreadLocalHolder.startTime.remove();
    }
}

3.3 自定义拒绝策略监控拒绝任务

public class MonitoredRejectedExecutionHandler implements RejectedExecutionHandler {
    private final AtomicLong rejectedCount = new AtomicLong();

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        long count = rejectedCount.incrementAndGet();
        Log.e("ThreadPool", "Task rejected, total rejected: " + count);
        // 可执行默认策略,如 CallerRunsPolicy
        new ThreadPoolExecutor.CallerRunsPolicy().rejectedExecution(r, executor);
    }

    public long getRejectedCount() { return rejectedCount.get(); }
}

3.4 自定义线程工厂监控线程创建

public class MonitoredThreadFactory implements ThreadFactory {
    private final AtomicInteger threadCount = new AtomicInteger();
    private final String namePrefix;

    public MonitoredThreadFactory(String namePrefix) { this.namePrefix = namePrefix; }

    @Override
    public Thread newThread(Runnable r) {
        int count = threadCount.incrementAndGet();
        Log.d("ThreadPool", "Creating thread #" + count);
        return new Thread(r, namePrefix + "-" + count);
    }

    public int getCreatedThreadCount() { return threadCount.get(); }
}

3.5 使用开源监控库(如 Dropwizard Metrics)

在 Java 后端开发中常用 Metrics 库来集成线程池监控,Android 中也可以引入(但注意包大小)。例如:

MetricRegistry metrics = new MetricRegistry();
ThreadPoolExecutor executor = ...;
metrics.register("executor", new ThreadPoolExecutorGaugeSet(executor));

然后通过 Slf4jReporter 定期输出日志。Android 中可自定义 Reporter 输出到 Logcat。


四、监控数据的分析与应用

4.1 判断线程池健康状态

  • 队列持续增长 → 任务生产速度 > 消费速度,可能需要增加核心线程数或优化任务。
  • 活跃线程数长期等于最大线程数 → 线程池满负荷,可能导致拒绝任务,可考虑扩大最大线程数或优化任务。
  • 拒绝任务数 > 0 → 系统过载,需调整参数或降级。
  • 任务执行时间过长 → 检查任务本身是否阻塞或耗时,考虑异步拆分。

4.2 动态调整线程池参数

根据监控数据,可以在运行时调整线程池参数:

// 增加核心线程数
executor.setCorePoolSize(newCoreSize);
// 设置最大线程数
executor.setMaximumPoolSize(newMaxSize);

但需要注意,调整参数可能引起线程池内部的重新分配,应谨慎操作,避免频繁调整。

4.3 报警机制

当监控指标超过阈值时,触发报警(如日志、崩溃上报、钉钉通知)。例如:

if (queueSize > 1000) {
    // 上报到 APM 平台
    Analytics.report("thread_pool_queue_full", queueSize);
}

五、注意事项

  • 监控本身的性能开销:采集指标的方法(如 getQueue().size())可能涉及同步,频繁调用会影响性能。建议监控频率不要太高(如10秒以上一次),或使用采样。
  • 线程安全ThreadPoolExecutor 的 get 方法是线程安全的,但组合指标(如同时获取队列大小和线程数)可能不是原子操作,但对于监控来说通常可以接受。
  • 内存泄漏:如果监控器持有线程池的引用且生命周期比线程池长,可能导致无法释放,需注意停止监控。
  • Android 组件生命周期:在 Activity/Fragment 中启动监控,记得在销毁时停止,避免后台任务泄漏。
  • 混淆与符号:如果使用自定义 Runnable 包装,注意混淆规则,保留相关类名以利于堆栈分析。

六、最佳实践示例

结合以上技巧,提供一个完整的线程池封装:

public class ObservableThreadPoolExecutor extends ThreadPoolExecutor {
    private final AtomicLong rejectedCount = new AtomicLong();
    private final ThreadLocal<Long> startTime = new ThreadLocal<>();

    public ObservableThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                                        TimeUnit unit, BlockingQueue<Runnable> workQueue,
                                        ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        startTime.set(System.currentTimeMillis());
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        Long start = startTime.get();
        if (start != null) {
            long cost = System.currentTimeMillis() - start;
            if (cost > 1000) { // 慢任务阈值1秒
                Log.w("ThreadPool", "slow task: " + r.getClass().getSimpleName() + " cost " + cost + "ms");
            }
        }
        startTime.remove();
    }

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        long count = rejectedCount.incrementAndGet();
        Log.e("ThreadPool", "task rejected, total=" + count);
        super.rejectedExecution(r, executor); // 保持默认策略
    }

    public long getRejectedCount() { return rejectedCount.get(); }

    public void printStats() {
        Log.d("ThreadPool", String.format(
            "pool=%d, active=%d, core=%d, max=%d, queue=%d, completed=%d, rejected=%d",
            getPoolSize(), getActiveCount(), getCorePoolSize(), getMaximumPoolSize(),
            getQueue().size(), getCompletedTaskCount(), getRejectedCount()));
    }
}

使用 ScheduledExecutor 定期调用 printStats() 即可。


七、总结

线程池监控是性能优化的基石。通过定期采集关键指标、监控任务耗时和拒绝情况,可以及时发现线程池的异常,并为调整参数提供依据。在 Android 开发中,应结合组件生命周期合理管理监控任务,避免性能损耗和内存泄漏。希望以上内容能帮助你构建自己的线程池监控体系,提升应用的稳定性和流畅度。