为什么使用ThreadPoolExecutor
在android开发中经常会使用多线程异步来处理相关任务,而如果用传统的newThread来创建一个子线程进行处理,会造成一些严重的问题:
1:在任务众多的情况下,系统要为每一个任务创建一个线程,而任务执行完毕后会销毁每一个线程,所以会造成线程频繁地创建与销毁。
2:多个线程频繁地创建会占用大量的资源,并且在资源竞争的时候就容易出现问题,同时这么多的线程缺乏一个统一的管理,容易造成界面的卡顿。
3:多个线程频繁地销毁,会频繁地调用GC机制,这会使性能降低,又非常耗时。
ThreadPoolExecutor
创建线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数
corePoolSize=>
线程池里的核心线程数量maximumPoolSize
=> 线程池里允许有的最大线程数量keepAliveTime=>
空闲线程存活时间unit=>
keepAliveTime的时间单位,比如分钟,小时等workQueue=> 缓冲
队列threadFactory=>
线程工厂用来创建新的线程放入线程池handler=>
线程池拒绝任务的处理策略,比如抛出异常等策略
一开始我们看到这些参数内心肯定是拒绝的,看源码也有些崩溃,那这些参数到底是些什么意思呢?
或者给一组实际数据:
corePoolSize:1
mamximumPoolSize:3
keepAliveTime:60s
workQueue:ArrayBlockingQueue,有界阻塞队列,队列大小是4
handler:默认的策略,抛出来一个ThreadPoolRejectException
这些代表什么呢?
参数可视化
我们把线程池比作一个花瓶
这个花瓶由 瓶口 、 瓶颈 、 瓶身 三个部分组成。
这三个部分分别对应着线程池的三个参数:maximumPoolSize, workQueue,corePoolSize。
改变corePoolSize
改变workQueue
线程池里的线程,我用一个红色小球表示,每来一个任务,就会生成一个小球:
而核心线程,也就是正在处理中的任务,则用灰色的虚线小球表示 (目前第一版动画先这样简陋点吧......)
我们往线程池中增加任务
于是画风就变成了这样,“花瓶”有这么几个重要的参数:
- corePoolSize=> 瓶身的容量
- maximumPoolSize=> 瓶口的容量
- keepAliveTime=> 红色小球的存活时间
- unit=> keepAliveTime的时间单位,比如分钟,小时等
- workQueue=> 瓶颈,不同类型的瓶颈容量不同
- threadFactory=> 你投递小球进花瓶的小手 (线程工厂)
- handler=> 线程池拒绝任务的处理策略,比如小球被排出瓶外
如果往这个花瓶里面放入很多小球时(线程池执行任务);
瓶身 (corePoolSize) 装不下了, 就会堆积到 瓶颈 (queue) 的位置;
瓶颈还是装不下, 就会堆积到 瓶口 (maximumPoolSize);
直到最后小球从瓶口溢出。
还记得上面提到的那一组实际参数吗,代表的花瓶大体上是如下图这样的:
那么参数可视化到底有什么实际意义呢?
阿里的规范
我们最开始 接触ThreadPoolExcutor的时候,一般使用Executors去创建线程池,但是阿里开发手册中对于 Java 线程池的使用规范:
一开始我并不知道为什么要去限制这样使用,这四种线程池为什么会导致OOM,
我们看看这四种线程池的具体参数,然后再用花瓶动画演示一下导致OOM的原因。
线程池FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
我们关心的参数如下
corePoolSize:nThreads
mamximumPoolSize:nThreads
workQueue:LinkedBlockingQueue
FixedThreadPool表示的花瓶就是下图这样子:
线程池SingleThreadPool:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
我们关心的参数如下
corePoolSize:1
mamximumPoolSize:1
workQueue:LinkedBlockingQueue
SingleThreadPool表示的花瓶就是下图这样子:
虽然两个线程池的样子没什么差异,但是这里我们发现了一个问题:
为什么 FixedThreadPool 和 SingleThreadPool 的 corePoolSize和mamximumPoolSize 要设计成一样的?
回答这个问题, 我们应该关注一下线程池的 workQueue 参数。
线程池FixedThreadPool和SingleThreadPool 都用到的阻塞队列 LinkedBlockingQueue。
LinkedBlockingQueue
/**
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}.
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
从LinkedBlockingQueue的源码注释中我们可以看到, 如果不指定队列的容量, 那么默认就是接近无限大的。
从动画可以看出, 花瓶的瓶颈是会无限变长的, 也就是说不管瓶口容量设计得多大, 都是没有作用的!
所以不管线程池FixedThreadPool和SingleThreadPool 的mamximumPoolSize 等于多少, 都是不生效的!
线程池CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
我们关心的参数如下
corePoolSize:0
mamximumPoolSize:Integer.MAX_VALUE
workQueue:SynchronousQueue
表示的花瓶就是下图这样子:
这里我们由发现了一个问题:
为什么CachedThreadPool的mamximumPoolSize要设计成接近无限大的?
回答这个问题, 我们再看一下线程池CachedThreadPool的 workQueue 参数:SynchronousQueue。
SynchronousQueue
来看SynchronousQueue的源码注释:
A synchronous queue does not have any internal capacity, not even a capacity of one.
从注释中我们可以看到, 同步队列可以认为是容量为0。一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。
所以如果mamximumPoolSize不设计得很大, 就很容易导致溢出。
但是瓶口设置得太大,堆积的小球太多,又会导致OOM(内存溢出)。
线程池ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
0, NANOSECONDS,
new DelayedWorkQueue());
}
我们关心的参数如下
corePoolSize:corePoolSize
mamximumPoolSize:Integer.MAX_VALUE
workQueue:DelayedWorkQueue
可以看到, 这里出现了一个新的队列 workQueue:DelayedWorkQueue
DelayedWorkQueue 是无界队列, 基于数组实现, 队列的长度可以扩容到 Integer.MAX_VALUE。
同时ScheduledThreadPool的 mamximumPoolSize 也是接近无限大的。
可以想象得到,ScheduledThreadPool就是史上最强花瓶, 极端情况下长度已经突破天际了!
到这里, 相信大家已经明白, 为什么这四种线程会导致OOM了。
线程池的状态
状态:
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
线程池状态含义:
-
RUNNING:接受新任务并且处理阻塞队列里的任务;
-
SHUTDOWN:拒绝新任务但是处理阻塞队列里的任务;
-
STOP:拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务;
-
TIDYING:所有任务都执行完(包含阻塞队列里面任务)当前线程池活动线程为 0,将要调用
terminated
方法; -
TERMINATED:终止状态,terminated方法调用完成以后的状态。
线程池状态转换:
1.RUNNING -> SHUTDOWN:显式调用 shutdown()
方法,或者隐式调用了 finalize()
,它里面调用了 shutdown()
方法。
2.RUNNING or SHUTDOWN -> STOP:显式调用 shutdownNow()
方法时候。
3.SHUTDOWN -> TIDYING:当线程池和任务队列都为空的时候。
4.STOP -> TIDYING:当线程池为空的时候。
5.TIDYING -> TERMINATED:当 terminated() hook
方法执行完成时候。
线程池处理流程和原理
我们先看提交任务的源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
通过上面可视化的展现几个参数,我们大概了解它们的意思
线程池的原理,当提交一个新的任务到线程池时,线程池的处理流程如下:
执行ThreadPoolExcutor的execute方法,可能会遇到以下情况:
- 如果线程池中的线程数未达到核心线程数,则创建核心线程处理任务。
- 如果线程数大于或者等于核心线程数,则将任务加入任务队列中,线程池中的空闲线程会不断的从任务队列中取出任务进行处理。
- 如果任务队列满了,并且线程数没有达到最大线程数,则创建非核心线程去处理任务。
- 如果线程数超过了最大线程数,则执行上面提到的几种饱和策略。
如何配置线程池:
- CPU密集型任务:尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
- IO密集型任务:可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
- 混合型任务:可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。