ThreadPoolExecutor
池化的目的是为了避免频繁的创建和销毁对象,减少对系统资源的消耗。
tomcat扩展了线程池核心类ThreadPoolExecutor,并重写了他的execute方法,定制了自己的任务处理流程。同时tomcat还实现了定制的任务队列,重写了offer方法,使得队列在无长度限制的情况下,线程池仍然有机会创建新的线程。
- Executors类提供了便捷声明线程池的方法,但是隐藏了线程池的参数细节。使用时,要根据场景配置线程数、任务队列、拒绝策略、线程回收策略,并对线程进行明确的命名方便排查问题。
- 使用了线程池是在复用,每次new 一个线程池出来比不用线程池还糟糕。
- 复用线程池不代表应用程序始终使用同一个线程池。
public ThreadPoolEwxcutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runable> workQueue,
ThreadFactory threadFactory,
RejectExectionHandler handler)
每次提交任务之后,线程数没有达到corePoolSize,线程池就会创建新的线程来执行。当线程池满了后,不会立即扩容线程池,而是放到工作队列workQueue中,线程池的线程努力的从workQueue拉活来干,调用poll方法获取任务。
如果任务很多,workQueue也满了,线程池创建临时线程来干活,当达到maximumPoolSize后,则不能创建新的临时线程,转而执行拒绝策略handler,或者抛出异常由调用线程来执行任务。
线程池的默认行为:
- 不会初始化corePoolSize个线程,有任务来了才初始化;
- 当线程满了之后不会立即扩容线程池,而是先把任务堆积到工作队列中;
- 当工作队列满了之后扩容线程池,一直到达maximumPoolSize为止;
- 当队列已满而且达到了最大线程之后还有任务进来,按照拒绝策略处理;
- 当线程数大于核心线程数,线程等待了keepAliveTime后还是没有任务需要处理的话,收缩到核心线程数。
拒绝策略
- AbortPolicy 抛出异常,拒绝新的任务处理
- CallerRunsPolicy 可以承受延迟且不能丢弃任何一个请求的话,可以选择,调用者执行自己的线程运行任务。
- DiscardPolicy 不接受新任务,直接丢弃
- DiscardOldestPolicy 丢弃最早未处理的任务请求
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, 2,
1, TimeUnit.HOURS,
new ArrayBlockingQueue<>(100),
new ThreadFactoryBuilder().setNameFormat("batchfileprocess-threadpool-%d").get(),
new ThreadPoolExecutor.CallerRunsPolicy());
上面的线程池不是异步的,因为执行了CallerRunsPolicy,在线程满载队列也满的情况下,任务会在调用execute方法的线程执行,异步任务变成了同步执行。
当线程池饱和的时候,计算任务在执行web请求的tomcat线程执行,会进一步影响到其它同步处理的线程,甚至造成整个应用程序崩溃。
private static ThreadPoolExecutor asyncCalcThreadPool = new ThreadPoolExecutor(
200, 200,
1, TimeUnit.HOURS,
new ArrayBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("asynccalc-threadpool-%d").get());
@GetMapping("right")
public int right() throws ExecutionException, InterruptedException {
return asyncCalcThreadPool.submit(calcTask()).get();
}
FixedThreadPool/CacheThreadPool
public static ExecutorService newFixedThreadPoll(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPoll() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
FixedThreadPool有固定长度的线程数组,忙不过来时会把任务放到无限长的队列里,LinkedBlockingQueue的队列长度是Integer.MAX_VALUE。如果任务较多且执行较慢的话,队列可能会快速挤压,导致OOM。
CachedThreadPool的maximumPoolSize的参数值是Integer.MAX_VALUE,因此对线程池的临时线程数不做限制,忙不过来时就无限创建临时线程,闲下来时再回收,任务队列是SynchronousQueue,队列长度是0。如果任务需要过长时间执行,大量进来的任务创建大量的线程,无限创建线程也会导致OOM。
- 错误代码案例:
class ThreadPoolHelper {
public static ThreadPoolExecutor getThreadPool() {
//线程池没有复用
return (ThreadPoolExecutor) Executors.newCachedThreadPool();
}
}
修复上面的bug的方法,使用静态变量来存放线程池的引用,返回线程池的代码直接返回静态字段。
- 修正后的代码
class ThreadPoolHelper {
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
10, 50,
2, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("demo-threadpool-%d").get());
public static ThreadPoolExecutor getRightThreadPool() {
return threadPoolExecutor;
}
}
Tomcat线程池
Java线程池是先用工作队列来存放来不及处理的任务,满了之后扩容线程池,如果队列设置的很大时,最大线程数这个参数显得没有意义,因为队列很难满,或者到满的时候再扩容线程池已经于事无补了。
如果让线程池更激进一些,优先开启更多的线程,再把队列当成一个后备方案。Tomcat的线程池就是基于此优化的。
思路:
- 由于线程池在工作队列满了无法入队的情况下会扩容线程池,可以重写offer方法,造成队列已满的假象。 2.到达最大线程后势必会触发拒绝策略,实现一个自定义拒绝策略处理程序,这个时候把任务真正插入队列。
//定制版的任务队列
taskqueue = new TaskQueue(maxQueueSize);
//定制版的线程工厂
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,
daemon, getThreadPriority());
//定制版的线程池
executor = new ThreadPoolExecutor(getMinSpareThreads(),
getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,
taskqueue, tf);