Android 多线程和线程池

773 阅读6分钟

AsyncTask

使用 AsyncTask

需要继承 AsyncTask,重写 doInBackground 方法。
onPreExecute 运行在调度线程
doInBackground 运行在线程池中
onPostExecute / onProgressUpdate / onCancelled 运行在 UI 线程中。

原理

AsyncTask 内部有两个静态线程池
一个线程池 SERIAL_EXECUTOR 表示串行线程池,内部包含一个消息队列,用来保存任务、按顺序调度任务
一个 THREAD_POOL_EXECUTOR 用来执行任务,其线程数量是根据 CPU 核数计算的
一个跑在主线程的 Handler,把执行进度和执行结果的回调发送到主线程
这两个线程池和一个 Handler 都是静态的,实际上是所有的 AsyncTask 对象公用的

特点和缺陷

  • 4.1之前版本需要在主线程完成初始化,现在的版本不需要初始化了
  • 5.0以及之前 AsyncTask 对象必须在主线程创建,execute 方法必须在主线程调用,之后的版本无此限制,但是 onPreExecute 方法会在调度线程执行
  • 一个 AsyncTask 只能执行一次,否则会报错
  • 1.6之前是串行任务,1.6开始采用线程池处理任务,3.0开始又串行任务,但可以使用 executeOnExecutor 并行
  • 一般会把 AsyncTask 写成 Activity 的内部类,注意内存泄漏问题
  • 新版本中并行线程池的核心线程设置了超时策略,闲置30秒会自动销毁,但旧版本核心线程不会被销毁

参考

Android源码分析—带你认识不一样的AsyncTask - 任玉刚
别再傻傻得认为AsyncTask只可以在主线程中创建实例和调用execute方法 - smileiam的专栏

HandlerThread

不使用 HandlerThread 创建异步 Handler的方法

class LooperThread extends Thread {
    public Handler mHandler;

    public void run() {
        Looper.prepare();
        mHandler = new Handler(){
            public void handlerMessage(Message msg){
                ...
            }
        };
        Looper.loop();
    }
}

HandlerThread 做了什么

    @Override
    public void run() {
        mTid = Process.myTid();
         // 为当前线程创建 Looper
        Looper.prepare();
        synchronized (this) {
            // 如果创建完成之前调用了 getLooper 方法,会被阻塞
            mLooper = Looper.myLooper();
            // mLooper 已经完成初始化,如果有线程调用 getLooper 方法且被阻塞,唤醒它
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        // 如果重写了 onLooperPrepared,会获得回调
        onLooperPrepared();
        // 进入循环
        Looper.loop();
        mTid = -1;
    }

HandlerThread 在 run 方法中创建 Looper 并开始循环,如果 Looper 创建完成之前调用 getLooper 会被阻塞,直到初始化完成。
另外 HandlerThread 提供了 quit 和 quitSafely 两个方法,用于停止其 Looper。

IntentService

使用方法

继承 IntentService,实现其 onHandleIntent 方法。

原理

IntentService 在 onCreate 方法中启动了 HandlerThread 并创建了异步 Handler,然后将 onStartCommend 方法转换到异步线程中执行,并回调给 onHandleIntent 方法。
IntentService 由于在异步线程执行,所以是串行的,并且执行完所有任务后会自动停止 Service。

线程池

线程池的优点

  1. 重用线程,自动创建、销毁线程,减少开销
  2. 能有效控制线程池的最大并发数,避免大量线程抢占系统资源导致阻塞
  3. 能够对线程进行简单的管理,并提供定时执行以及定时循环执行等功能

ThreadPoolExecutor

状态和生命周期

  • RUNNING 运行中,可以接受新任务并处理
  • SHUTDOWN 关闭状态,不会接受新任务,但是会处理队列中还存在的任务
  • STOP 停止状态,不会接受新任务,也不处理队列中的任务,直接中断
  • TIDYING 停止状态,表示所有任务已经终止了
  • TERMINATED 表示 terminated 方法执行完了

构造方法

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

参数说明:

  • corePoolSize: 核心线程数量,默认会一直存活。如果 allowCoreThreadTimeOut 设置为 true,核心线程也会超时中止。
  • maximumPoolSize: 最大线程数量,活动线程达到这个数值后,后续的新任务将会被阻塞。
  • keepAliveTime 和 unit: 非核心线程闲置时的超时时间,超过这个时长非核心线程就会被回收。当 allowCoreThreadTimeOut 属性设置为 true 时,keepAliveTime 同样会作用于核心线程。
  • workQueue: 线程池中的任务队列,通过线程池的 execute 方法提交的 Runnable 对象会存储在这个参数中。
  • threadFactory: 线程工厂,可以在创建线程时指定线程名称。
  • handler: 无法执行任务时的拒绝策略,有以下几种
    • AbortPolicy 默认选项,直接抛出异常;
    • CallerRunsPolicy 在调用线程中执行;
    • DiscardPolicy 直接丢弃新任务;
    • DiscardOldestPolicy 丢弃最旧的任务;

工作流程

当有新的任务进入线程池中时:

  1. 如果线程数量未达到核心线程的数量,会直接启动一个核心线程;
  2. 如果线程数量已经达到或超过核心线程数量,那么任务会被插入到任务队列中等待执行;
  3. 如果无法插入到任务队列中(往往由于任务队列已满),如果线程数量为达到最大值,会启动非核心线程;
  4. 如果任务队列已满,而且线程数量已经达到最大数量,就会按照拒绝策略执行。

线程池的分类

Executors 提供了几种常用的线程池:

  1. FixedThreadPool 线程数量固定,核心线程数量和最大线程数量相等,没有非核心线程也没有超时机制,队列无限大;
  2. CachedThreadPool 没有核心线程,线程数量无限大,超时时间一分钟,使用 SynchronousQueue 队列存储任务,实际上不会存储任务;
  3. ScheduledThreadPool 核心线程固定,非核心线程无限大,超时时间是0,马上被回收,用于定时任务或周期循环任务;
  4. SingleThreadExecutor 只有一个核心线程,没有超时,队列无限大。

注意事项

  1. 阿里 Java 开发手册中禁止手动创建线程:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗过多资源或者“过度切换”的问题。
  2. 阿里 Java 开发手册同样禁止使用 Executors 去创建线程,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,避免资源耗尽的风险。
    原因在于 FixedThreadPool 和 SingleThreadPool 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,导致 OOM;CachedThreadPool 和 ScheduledThreadPool 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,导致 OOM。

参考资料

重走JAVA之路(五):面试又被问线程池原理?教你如何反击 - 掘金