线程池的优点
- 重用线程池的线程,减少线程创建和销毁带来的性能开销
- 控制线程池的最大并发数,避免大量线程互相抢系统资源导致阻塞
- 提供定时执行和间隔循环执行功能
线程池的参数简介
- corePoolSize: 线程池的核心线程数,默认情况下,核心线程会一直存活(设置了超时机制除外, allowCoreThreadTimeOut属性为true时开启)
- maxinmumPoolSize: 线程池能容纳的最大线程数,当活动的线程达到这个数值之后,后续新任务会被阻塞
- keepAliveTime: 非核心线程闲置的超时时长,超过这个时长,非核心线程就会被回收,当allowCoreThreadTimeOut为true时,keepAliveTime同样作用于核心线程。
- unit:keepAliveTime的时间单位,这是一个枚举,常用有TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)
- workQueue: 线程池中的任务队列,通过execute方法提交的Runnable对象会存储在这个参数中
- threadFactory: 线程工厂,为线程池提供创建线程的功能,是个接口,提供Thread newThread(Runnable r)方法
- RejectedExecutionHandle:当线程池无法执行新任务时,可能由于线程队列已满或无法成功执行任务,这时候 ThreadPoolExecutor会调用handler的 rejectedExecution的方法,默认会抛出RejectedExecutionException
线程池执行任务的流程
- 如果线程池中的线程数量未达到核心线程的数量,那么会直接启动一个核心线程来执行任务
- 如果线程池中的线程数量已经达到或超过核心线程数量,那么任务会被插入到任务队列中排队等待执行
- 如果步骤2中无法将任务插入到任务队列中,往往是因为任务队列已满,这个时候如果线程数量未达到线程池规定的最大值,那么会立刻启动一个非核心线程来执行任务
- 如果步骤3中线程数量达到线程池规定的最大值,线程池会拒绝执行任务,执行拒绝策略。
线程池的四种拒绝策略
- 默认是AbordPolicy:丢弃任务抛出RejectedExecutionException异常。
- CallerRunsPolicy:由调用线程(提交任务的线程)直接执行此任务
- 此策略提供简单的反馈控制机制,能够减缓 新任务的提交速度。
- DiscardPolicy:不能执行的任务,并将该任务删除。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务
- 特点可以概括为:“喜新厌旧”
常见的四种线程池
-
FixedThreadPool:线程数量固定的线程池,所有线程都是核心线程。当所有线程都处于活动状态时,新任务会处于等待状态,只有核心线程并且不会回收(无超时机制),能快速的响应外界请求。
- 阻塞队列为无界队列LinkedBlockingQueue
-
CachedThreadPool:线程数量不定的线程池,最大线程数为Integer.MAX_VALUE(相当于任意大),只有非核心线程。当所有线程都处于活动状态时,会创建新线程来处理任务;线程池的空闲进程超时时长为60秒,超过就会被回收;任何任务都会被立即执行,适合执行大量的耗时较少的任务。
- 阻塞队列是SynchronousQueue,无容量的队列,立刻执行
-
ScheduledThreadPool:核心线程数量固定,非核心线程数量无限制,非核心线程闲置时会被立刻回收,用于执行定时任务和具有固定周期的重复任务。
- 阻塞队列是无界的DelayedWorkQueue,DelayedWorkQueue会将任务进行排序,先要执行的任务放在队列的前面
-
SingleThreadExecutor:只有一个核心线程,所有任务都在这个线程中串行执行,不需要处理线程同步问题
- 阻塞队列是LinkedBlockingQueue,保证顺序执行
线程池的阻塞队列
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
- 可设置容量队列,不设置的话是无界的,可能OOM
- 吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- 它是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。这里可以自定义实现 compareTo()方法来指定元素进行排序规则;或者初始化PriorityBlockingQueue时,指定构造参数 Comparator来对元素进行排序。但其不能保证同优先级元素的顺序。
- DelayQueue:使用优先级队列实现的无界阻塞队列。
- 根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
- SynchronousQueue:不存储元素的阻塞队列。
- 不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。
- LinkedTransferQueue:由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列。
如何合理配置线程池参数?
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
- 任务的性质:CPU 密集型任务,IO 密集型任务和混合型任务。
- 任务的优先级:高,中和低。
- 任务的执行时间:长,中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
任务性质不同的任务可以用不同规模的线程池分开处理。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中进行缓存等待。在这里限制了同一时间并发请求的数量。