从阿里规约谈起 - 禁用 Executors 创建线程池

前言

Android 开发中耗时任务应该放在子线程中进行,否则会阻塞 UI 造成 ANR。但是如果直接创建子线程,阿里规约会提示:

直接使用子线程报错

关于禁止直接创建线程的原因如图,不再赘述。

Executors 被禁用原因

当我们转而选择线程池的时候,JDK 提供了一个简单的工厂模式来创建:Executors

使用非常简单,四个方法可以创建四个不同类型的线程池,基本满足大部分线程池的使用需求。

 1//创建固定数目线程的线程池
 2Executors.newFixedThreadPool(int nThreads);
 3
 4//创建一个唯一线程的线程池
 5Executors.newSingleThreadExecutor()
 6
 7//创建一个可缓存的线程池
 8Executors.newCachedThreadPool()
 9
10//创建一个支持定时及周期性的任务执行的线程池
11Executors.newScheduledThreadPool(int corePoolSize)

但是如果使用了,会发现阿里规约再次禁止:

使用 Executors 报错 约定禁止的原因如下:

对于 newFiexedThreadPoolnewSingleThreadExecutor

1public static ExecutorService newFixedThreadPool(int nThreads) {
2    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
3}
1public static ExecutorService newSingleThreadExecutor() {
2    return new FinalizableDelegatedExecutorService
3            (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()));
4}

原因是请求队列为 LinkedBlockingQueue 的默认实现,跟踪代码可以看到:

1public LinkedBlockingQueue() {
2    this(Integer.MAX_VALUE);
3}

在其默认实现中,队列长度为 Integer 最大值。

对于 newCachedThreadPoolnewScheduledThreadPool

1public static ExecutorService newCachedThreadPool() {
2    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
3}
1public ScheduledThreadPoolExecutor(int corePoolSize) {
2    super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue());
3}

原因是最大线程数为 Integer 最大值。

这四个方法如果直接使用默认模式,无一幸免均可能因为线程太多而造成 OOM。

使用 ThreadPoolExecutor 手动创建线程池

综上,我们需要使用 ThreadPoolExecutor 手动创建线程池。

ThreadPoolExecutor 一般需要关心的参数如下:

  • corePoolSize:核心线程数量
  • maximumPoolSize:最大线程数量
  • keepAliveTimeunit:非核心线程空闲后的存活时间
  • workQueue:保存等待执行的任务的阻塞队列
  • handler:队列和最大线程都满了之后的饱和策略

其中 workQueue 的常用类型为:

  • SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它
  • LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数等于核心线程数,则进入队列等待。由于这个队列默认没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了 maximumPoolSize 的设定失效,因为总线程数永远不会超过 corePoolSize
  • ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果当前线程数等于核心线程数,则入队等候,如果队列已满,则新建非核心线程执行任务
  • DelayQueue:队列内元素必须实现 Delayed 接口,这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

线程池执行任务时的策略如下:

  1. 线程数量未达到 corePoolSize,则新建一个线程(核心线程)执行任务
  2. 线程数量达到了 corePools,则将任务移入队列等待
  3. 队列已满,新建线程(非核心线程)执行任务
  4. 队列已满,总线程数又达到了 maximumPoolSize,就会由 handler 执行饱和策略(一般是抛出异常)

协程

对于现在的开发来说最简单的方案应该是换 Kotlin 使用协程,Kotlin 内置了多个线程池给我们用:

  • Dispatchers.Main - 主线程,用于界面更新。
  • Dispatchers.IO - I/O 线程池。用于读写数据库和网络请求。
  • Dispatchers.Default - CPU 线程池。用于重 CPU 操作。