ThreadPoolExecutor线程池
昨天去百趣网面试,面试官问了很多问题,其中有部分是关于线程池的。因为只是用过,但是对其中的原理和应用场景还是一知半解,痛定思痛下重新学习一波。
线程池的执行策略
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//1.如果当前运行的线程少于corePoolSize,则创建新线程来执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//2.如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue
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);
}
//3.如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务
//4.如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用饱和策略
else if (!addWorker(command, false))
reject(command);
}
线程池的参数说明
new ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize(线程池的基本大小)
当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。
如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
- maximumPoolSize(线程池的最大大小)
即整个线程池的最大容量,maximumPoolSize >= corePoolSize。
如果队列runnableTaskQueue采用无限队列(不建议采用无限队列)时,则不会再去判断当前线程数是否大于maximumPoolSize了。此时,maximumPoolSize参数和handler参数均失效,永远不会再用到了。
- keepAliveTime(活跃时间)
corePoolSize里的线程在默认情况下永远是活跃的(即永远不会变成TERMINATED状态),除非allowCoreThreadTimeOut参数设为true,corePoolSize里的线程才会用到这个keepAliveTime,当空闲线程活跃时间过后,变为TERMINATED状态。
maximumPoolSize除了corePoolSize外的线程,如果空闲时间超过了keepAliveTime,线程状态变为TERMINATED状态。
所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
- unit(活跃时间的时间单位)
一般用毫秒比较多,当然还有年、月、日、时、分、秒、微妙、纳秒等等
- workQueue(存储线程任务的队列)
用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
强烈建议使用有界队列!!
- threadFactory(线程工厂)
用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
- handler(饱和策略)
当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略:
- AbortPolicy:直接抛出异常。
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。
监控线程池的小技巧
通过继承线程池ThreadPoolExecutor来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。
例如:监控任务的平均执行时间、最大执行时间和最小执行时间等。
//该方法执行于某个线程任务执行前
protected void beforeExecute(Thread t, Runnable r) { }
//该方法执行于某个线程任务执行结束后
protected void afterExecute(Runnable r, Throwable t) { }
//该方法执行于整个线程池中止后
protected void terminated() { }
jdk提供的几种常用的线程池选择
-
FixedThreadPool(固定线程数的线程池)
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
只有核心线程数,没有多余的线程(corePoolSize==maximumPoolSize),即多于核心线程数的任务直接存入列队中,等待核心线程空闲后执行。
它适用于任务时间较长,或者负载比较重的服务器
-
SingleThreadExecutor(单个线程的线程池)
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1,其他参数与FixedThreadPool相同。
它适用于需要保证顺序地执行各个任务,并且在任意时间点,不会有多个线程是活动的应用场景。
-
CachedThreadPool(大小无界的线程池)
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
核心线程数被设置为0,maximumPoolSize被设置为Integer.MAX_VALUE,可以被认为是无界的。keepAliveTime设置为60L,意味着空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。SynchronousQueue作为线程池的工作队列,但maximumPoolSize是无界的,这意味着如果主线程提交任务的速度高于线程池中线程处理任务的速,线程池会不断创建新线程。极端情况下,线程池会因为创建过多线程而耗尽CPU和内存资源。
这是一个根据需要创建新线程的线程池。它适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。
- ScheduledThreadPoolExecutor
它适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景。
- SingleThreadScheduledExecutor
它适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。
notet小贴士:最后两种线程池功能与Timer类似,主要用来在给定的延迟之后运行任务,或者定期执行任务。本人表示没用过,没有实践经验。
线程池的几种状态
- RUNNING:接受新任务并处理排队任务
- SHUTDOWN:不接受新任务,但处理排队任务
- STOP:不接受新任务,不处理排队任务,并中断正在进行的任务
- TIDYING:所有任务都已终止,workerCount为零,转换到状态TIDYING的线程将运行terminate()钩子方法
- TERMINATED:terminate()已完成
作用:比如可以比较下shutdown()方法和shutdownNow()的区别,其实就是修改状态不一样,以及返回值的区别。