前言
接手了一个项目,用了 ThreadPoolExecutor自定义了线程池,然后项目中异步的任务就都采用它来运行~然而,在众多的异步任务中,有那么一个任务是会开启无头浏览器的。这个东西是占资源的,周末数量量大的时候,大量线程都执行了此任务,造成CPU、内存资源占用迅速飙升,从而影响到其他正常服务,应用挂了......
解决方式
都是异步任务了,此等占用资源的服务,采用1~2两个线程去跑就完事情了,严格控制某个时刻打开无头浏览器的线程数目。于是重新开一个较小的线程池,专门跑该服务。
扯扯5个线程池
前四种线程池本质上都是用的ThreadPoolExecutor,通过对ThreadPoolExecutor的7个参数的灵活配置,达到不同的效果。这7个参数如下:
int corePoolSize, // 核心线程池数目
int maximumPoolSize, // 线程池线程存在的最大数据
long keepAliveTime, // 除了核心线程、其他线程空闲时的存活时间
TimeUnit unit, // 时间单位
// 阻塞队列,线程满时,此队列可以继续存放对应的任务
BlockingQueue<Runnable> workQueue,
// 线程工厂, 专门用来创建线程, 可以通过实现ThreadFactory接口自定义一个线程工厂(包括自定义线程名称)
ThreadFactory threadFactory,
// 线程池满且阻塞队列到达边界时,使用对应的拒绝策略来处理接下来的请求
RejectedExecutionHandler handler
5中线程池如下,第5个是JDK1.8添加的,采用的是ForkJoinPool进行实现,该线程池思想可以参考归并算法,其适合解决计算类型(CPU密集型)的任务场景。
IO场景也是可以的,不过可能“IO读”的场景比较适合,之前查库时,由于查询的数量量大,通过此方式进行“分治-合并”的查询
- newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
- newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
- newWorkStealingPool:JDK1.8增加的一个线程池,直译过来时工作窃取线程池,其实就是当有多个任务时,先完成的任务可以合并其他的线程的的执行结果。
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
四种拒绝策略
关于拒绝的方式,JDK提供了RejectedExecutionHandler
接口,我们也可以实现该接口自定义拒绝方式,而在JDK中则提供了四种RejectedExecutionHandler
的实现。
- AbortPolicy:该方式是直接抛出异常,也是默认的实现方式。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
- CallerRunsPolicy:该策略适合于任务的的重要程度是比较高的。当线程池满的时候,会由调用线程池的线程去行任务。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
- DiscardPolicy:放弃策略,什么都不做也不说
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
- DiscardOldestPolicy:放弃追来策略
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
异常处理
某个线程中断, ThreadPoolExecutor如何处理?这里需要分为两种情况进行讨论,一种是submit提交任务,一种是通过execute的方式,直接执行任务。如图:
直接报错:
如果通过submit方式提交任务,你会发现没有错误信息进行输出。
此时,可以换成这种方式,通过future里头的get方法h=取出异常:
究其原因我们可以对比源码,在execute里头,在runWorker方法源码里头,我们可以看到task.run()
这里。
对于execute方式来说,直接就是跑ThreadDemo1实现了Runnable的run方法,然后抛出异常,catch住后,继续抛出,然后执行finally里头的afterExecute
方法,该方法默认中是没有具体实现的。
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run(); // 执行任务
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
对于submit来说,由于经过一层封装,类似适配器模式,此时来到task.run()
是,这个task看似是个Runnablele类型,其本质是FutureTask,此时执行的是FutureTask里的run方法。
如图,Runnable经过适配器,已经和callable实现转换。
最终,把异常设置进去FutureTask中,完成异常的传递。
线程“坏了”咋办
线程如果出现了了异常,那线程池还要怎样处理它呢?我们都知道线程池的一个好处就是线程复用了,那线程“坏了”如出异常了,线程池如何处置呢?
从源码我们可以得出线程坏了的时候,它会执行这段源码processWorkerExit(w, completedAbruptly)
,如下,通过workers.remove(w)
移除线程,然后又通过addWorker
重新新建线程~
怎样移除呢?原来所有的work线程都是通过一个HashSet进行保存的,而HashSet的移除又是用的hashMap方法,最后的原理还是跟HashMap一致,将value置为了null,那key是谁呢?当然就是Worker线程对象本身。
ps:关于key这里,小伙伴们还会想到哪些知识点呢?
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
// 移除线程
workers.remove(w);
} finally {
mainLock.unlock();
}
tryTerminate();
int c = ctl.get();
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
// 添加线程
addWorker(null, false);
}
}
关于线程池的大小设置
之前也是各种网上找资料,很多都是如下的设置方案,但只能说在无法判断具体场景的使用情况时,你可以尝试这么设置。实际效果还是需要结合用户访问情况、服务器配置情况来进行设置。
-
CPU密集型任务主要消耗CPU资源进行计算, 当任务为CPU密集型时,核心池线程数设置为CPU核数+1
-
IO密集型一般设置为CPU核心两倍,
总线程数量可以设置为核心线程两倍。
以目前接受的项目的使用情况,在4C8G的服务器配置下,核心线程设置为20的情况下,效果甚加,其他线程大小也设置为20(考虑到某个时刻下,核心线程全部挂了,此时我还有最多20个线程可以创建)
ps: 拓展:为何IO密集型一般设置为CPU核心两倍? 关键词:超线程