Android开发中的线程池

401 阅读7分钟

线程池的优点

  1. 重用线程池的线程,减少线程创建和销毁带来的性能开销
  2. 控制线程池的最大并发数,避免大量线程互相抢系统资源导致阻塞
  3. 提供定时执行和间隔循环执行功能

线程池的参数简介

  1. corePoolSize: 线程池的核心线程数,默认情况下,核心线程会一直存活(设置了超时机制除外, allowCoreThreadTimeOut属性为true时开启)
  2. maxinmumPoolSize: 线程池能容纳的最大线程数,当活动的线程达到这个数值之后,后续新任务会被阻塞
  3. keepAliveTime: 非核心线程闲置的超时时长,超过这个时长,非核心线程就会被回收,当allowCoreThreadTimeOut为true时,keepAliveTime同样作用于核心线程。
  4. unit:keepAliveTime的时间单位,这是一个枚举,常用有TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)
  5. workQueue: 线程池中的任务队列,通过execute方法提交的Runnable对象会存储在这个参数中
  6. threadFactory: 线程工厂,为线程池提供创建线程的功能,是个接口,提供Thread newThread(Runnable r)方法
  7. RejectedExecutionHandle:当线程池无法执行新任务时,可能由于线程队列已满或无法成功执行任务,这时候 ThreadPoolExecutor会调用handler的 rejectedExecution的方法,默认会抛出RejectedExecutionException

线程池执行任务的流程

线程池

  1. 如果线程池中的线程数量未达到核心线程的数量,那么会直接启动一个核心线程来执行任务
  2. 如果线程池中的线程数量已经达到或超过核心线程数量,那么任务会被插入到任务队列中排队等待执行
  3. 如果步骤2中无法将任务插入到任务队列中,往往是因为任务队列已满,这个时候如果线程数量未达到线程池规定的最大值,那么会立刻启动一个非核心线程来执行任务
  4. 如果步骤3中线程数量达到线程池规定的最大值,线程池会拒绝执行任务,执行拒绝策略。

线程池的四种拒绝策略

  • 默认是AbordPolicy:丢弃任务抛出RejectedExecutionException异常
  • CallerRunsPolicy:由调用线程(提交任务的线程)直接执行此任务
    • 此策略提供简单的反馈控制机制,能够减缓 新任务的提交速度。
  • DiscardPolicy:不能执行的任务,并将该任务删除。
  • DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务
    • 特点可以概括为:“喜新厌旧”

常见的四种线程池

  1. FixedThreadPool:线程数量固定的线程池,所有线程都是核心线程。当所有线程都处于活动状态时,新任务会处于等待状态,只有核心线程并且不会回收(无超时机制),能快速的响应外界请求

    1. 阻塞队列为无界队列LinkedBlockingQueue
  2. CachedThreadPool:线程数量不定的线程池,最大线程数为Integer.MAX_VALUE(相当于任意大),只有非核心线程。当所有线程都处于活动状态时,会创建新线程来处理任务;线程池的空闲进程超时时长为60秒,超过就会被回收;任何任务都会被立即执行,适合执行大量的耗时较少的任务

    1. 阻塞队列是SynchronousQueue,无容量的队列,立刻执行
  3. ScheduledThreadPool:核心线程数量固定,非核心线程数量无限制,非核心线程闲置时会被立刻回收,用于执行定时任务和具有固定周期的重复任务

    1. 阻塞队列是无界的DelayedWorkQueue,DelayedWorkQueue会将任务进行排序,先要执行的任务放在队列的前面
  4. SingleThreadExecutor:只有一个核心线程,所有任务都在这个线程中串行执行,不需要处理线程同步问题

    1. 阻塞队列是LinkedBlockingQueue,保证顺序执行

线程池的阻塞队列

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
    • 可设置容量队列,不设置的话是无界的,可能OOM
    • 吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
    • 它是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。这里可以自定义实现 compareTo()方法来指定元素进行排序规则;或者初始化PriorityBlockingQueue时,指定构造参数 Comparator来对元素进行排序。但其不能保证同优先级元素的顺序。
  • DelayQueue:使用优先级队列实现的无界阻塞队列。
    • 根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
  • SynchronousQueue:不存储元素的阻塞队列。
    • 不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

如何合理配置线程池参数?

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  1. 任务的性质:CPU 密集型任务,IO 密集型任务和混合型任务。
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。CPU 密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO 密集型任务则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu

我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。

优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,如果等待的时间越长 CPU 空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用 CPU。

并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。

OkHttp源码中的线程池解析

  • OkHttp是Android开发常用的网络请求库,为了保证并发网络请求,内部使用到了线程池。
  • OkHttp创建线程池的源码如下:
 executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, 
                                            TimeUnit.SECONDS,
                                            new SynchronousQueue<Runnable>(),
                                            Util.threadFactory("OkHttp Dispatcher", 
                                            false));

按顺序每个参数解析:

0: 核心线程数为0,线程池内全部都是非核心线程。

Integer.MAX_VALUE:线程池可以容纳最大线程数量。Integer.MAX_VALUE是非常大的一个数,可以理解为OkHttp随时可以创建新的线程来满足需要。可以保证网络的I/O任务有线程来处理,不被阻塞。

60,TimeUnit.SECONDS:空闲线程被终止的等待时间,这里设置为60秒。

new SynchronousQueue():阻塞队列。SynchronousQueue是这样 一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。

Util.threadFactory("OkHttp Dispatcher", false):用来创建线程的线程工厂。

  • 从参数上可以看出,OkHttp的线程池和上面介绍到的CacheThreadPool很类似。

  • 但网络请求支持的并发数量并不是Integer.MAX_VALUE这么多,OkHttp源码中关于异步请求:

synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
    runningAsyncCalls.add(call);
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}

当正在运行的异步请求队列中的数量小于64并且正在运行的请求主机数小于5时则把请求加载到runningAsyncCalls中并在线程池中执行,否则就再入到readyAsyncCalls中进行缓存等待。在这里限制了同一时间并发请求的数量。