21、AsyncTask与线程池:AsyncTask使用与缺陷、ThreadPoolExecutor配置、Callable与Future

说到Android里的异步任务,很多老开发都会叹口气。AsyncTask,这个曾经被写进官方文档的“亲儿子”,如今已经被标记为废弃。我刚开始做Android那会儿,几乎每个网络请求都用它。后来踩了坑,才明白为什么官方要放弃它。

说白了,AsyncTask的问题在于——它太“黑盒”了。你只管往里面塞任务,它怎么调度、怎么并发,你完全控制不了。这在简单场景下没问题,但项目一复杂,各种诡异Bug就冒出来了。

AsyncTask的“罪与罚”

先说说AsyncTask的正确用法。它本质上是一个封装好的异步框架,内部维护了一个线程池和一个Handler。你只需要继承它,实现几个回调方法就行。

private class DownloadTask extends AsyncTask<String, Integer, String> {
    @Override
    protected void onPreExecute() {
        // 主线程,开始前调用
        progressBar.setVisibility(View.VISIBLE);
    }

    @Override
    protected String doInBackground(String... urls) {
        // 子线程,真正干活的地方
        String result = "";
        for (int i = 0; i < urls.length; i++) {
            result += downloadUrl(urls[i]);
            publishProgress((i + 1) * 100 / urls.length);
        }
        return result;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        // 主线程,更新进度
        progressBar.setProgress(values[0]);
    }

    @Override
    protected void onPostExecute(String result) {
        // 主线程,任务完成
        textView.setText(result);
        progressBar.setVisibility(View.GONE);
    }
}

嗯,这里要注意——doInBackground是在子线程跑的,其他三个回调都在主线程。这个设计本身没问题,问题出在它的线程池策略上。

⚠️ 我曾经踩过的坑:
  • 内存泄漏:AsyncTask持有Activity的引用,如果任务还没完成Activity就被销毁了,那这个Activity就泄漏了。我有个项目因此OOM崩溃了好几次。
  • 串行执行:在Android 1.6之后,AsyncTask默认是串行执行的。你同时开10个任务,它一个一个跑。你以为在并发,其实在排队。
  • 生命周期不同步:任务在后台跑着,用户旋转了屏幕,Activity重建了。这时候onPostExecute更新的是旧的View,直接崩溃。
  • 结果丢失:Activity被销毁后,AsyncTask的结果没人接收了。你辛辛苦苦下载完,发现没地方展示。

为什么会这样?因为AsyncTask把线程管理和UI更新的逻辑耦合在一起了。你想想看,一个工具类既要管线程调度,又要管生命周期,还要管UI刷新——它管得过来吗?

ThreadPoolExecutor:把控制权拿回来

我个人习惯,从2015年开始就全面转向ThreadPoolExecutor了。它才是真正让你掌控一切的线程池方案。

先看一个我常用的配置模板:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,                      // corePoolSize:核心线程数
    4,                      // maximumPoolSize:最大线程数
    30,                     // keepAliveTime:空闲线程存活时间
    TimeUnit.SECONDS,       // 时间单位
    new LinkedBlockingQueue<Runnable>(128),  // 任务队列
    Executors.defaultThreadFactory(),        // 线程工厂
    new ThreadPoolExecutor.AbortPolicy()     // 拒绝策略
);

这几个参数怎么配?我简单说说我的经验:

参数 说明 我的建议
corePoolSize 一直活着的线程数 CPU密集型设为核心数+1,IO密集型设为核心数*2
maximumPoolSize 最多能创建的线程数 不要超过核心数的2倍,否则线程切换开销太大
keepAliveTime 超过核心数的线程空闲多久后被回收 30秒到60秒比较合适
workQueue 任务排队的地方 有界队列,防止内存被撑爆
RejectedExecutionHandler 队列满了怎么办 AbortPolicy直接抛异常,或者自定义策略记录日志
💡 避坑指南: 我曾经把maximumPoolSize设成20,结果手机直接卡死。线程不是越多越好,线程切换是有成本的。你想想看,一个4核CPU,同时跑20个线程,大部分时间都在切换上下文,真正干活的时间反而少了。

Callable与Future:带返回值的异步任务

Runnable有个硬伤——它没有返回值。你需要在外面定义一个变量,然后在Runnable里面赋值,再用各种同步机制去读。麻烦不麻烦?

Callable就是来解决这个问题的。它跟Runnable很像,但可以返回结果,还能抛异常。

Callable<String> callable = new Callable<String>() {
    @Override
    public String call() throws Exception {
        // 模拟耗时操作
        Thread.sleep(2000);
        return "下载完成";
    }
};

Future<String> future = executor.submit(callable);

// 做其他事情...

try {
    String result = future.get(); // 阻塞,直到拿到结果
    Log.d("TAG", result);
} catch (ExecutionException e) {
    // 处理call()里抛出的异常
    Log.e("TAG", "任务执行异常", e);
} catch (InterruptedException e) {
    // 线程被中断
    Log.e("TAG", "任务被中断", e);
}

Future的get()方法是阻塞的。如果你不想一直等着,可以用get(long timeout, TimeUnit unit)设置超时时间。超时了就抛TimeoutException,你可以决定是重试还是放弃。

还有个实用方法——future.cancel(true)。我在项目中遇到过一种情况:用户下拉刷新时,如果上一个请求还没回来,就直接取消掉。用cancel方法就很方便。

📌 核心要点:
  • AsyncTask已废弃,不要再在新项目中使用
  • ThreadPoolExecutor是线程池的正确打开方式,参数要按需配置
  • Callable+Future适合需要返回结果的异步任务
  • 记得给线程池设置有界队列,防止内存溢出
  • Activity销毁时,记得取消正在执行的任务

最后说一句,线程池不是银弹。如果你的任务很简单,用Handler+Thread就够了。但如果你需要管理多个并发任务、控制并发数、获取返回值——那ThreadPoolExecutor+Callable就是你的不二之选。

Android 异步任务方案对比 AsyncTask 已废弃,不推荐使用 ThreadPoolExecutor 推荐,灵活可控 Callable + Future 带返回值的异步 主要缺陷 • 内存泄漏 • 串行执行 • 生命周期不同步 • 结果丢失 核心配置参数 • corePoolSize • maximumPoolSize • keepAliveTime • workQueue 核心方法 • call() 执行任务 • get() 获取结果 • cancel() 取消任务 • isDone() 检查完成 推荐方案:ThreadPoolExecutor + Callable/Future
公众号:蓝海资料掘金营,微信deep3321