线程池大家都很熟悉,无论是平时的业务开发还是框架中间件都会用到,大部分都是基于JDK线程池ThreadPoolExecutor做的封装,

都会牵涉到这几个核心参数的设置:核心线程数,等待(任务)队列,最大线程数,拒绝策略等。

但如果线程池设置不当就会引起一系列问题, 下面就说下我最近碰到的问题。

案件还原

比如你有一个项目中有个接口部分功能使用了线程池,这个功能会去调用多个第三方接口,都有一定的耗时,为了不影响主流程的性能,不增加整体响应时间,所以放在线程池里和主线程并行执行,等线程池里的任务执行完通过future.get的方式获取线程池里的线程执行结果,然后合并到主流程的结果里返回,大致流程如下:

线程池参数为:

  • coresize:50
  • max:200
  • queuesize:1
  • keepalivetime:60s
  • 拒绝策略为reject

假设每次请求提交5个task到线程池,平均每个task是耗时50ms

没过一会就收到了线程池满了走了拒绝策略的报错

结合你对线程池的了解,先思考下为什么

线程池的工作流程如下:

根据这个我们来列一个时间线

1. 项目刚启动 第1次请求(每次5个task提交到线程池),创建5个核心线程

2. 第2次请求 继续创建5个(共10个核心线程了)

3. 直到第10次 核心线程数会达满50个

4. 核心线程处理完之后核心线程会干嘛呢

根据 jdk1.8的线程池的源码:

线程池的线程处理处理了交给它的task之后,它会去getTask()

源码如下:

private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out? for (;;) {
int c = ctl.get();
int rs = runStateOf(c); // Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
} int wc = workerCountOf(c); // Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
} try {
//注意这段
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}

请注意上面代码中的bool类型的timed的赋值逻辑,

由于allowCoreThreadTimeOut默认为false,也就是说:

只要创建的线程数量超过了核心线程数,那么干完手上活后的线程(不管是核心线程,还是超过队列后新开的线程)就会走进

//线程状态为 timedwaiting
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)

由于我们上面步骤里面还没有超过coresize所以会走进

//线程状态为 waiting
workQueue.take()

所以答案是:上面步骤干活的核心线程处理完之后核心线程会进入waiting状态,

只要队列一有活就会被唤醒去干活。

5. 到第11次的时候

好家伙,到这步骤的时候 ,核心线程数已满,那么就往队列里面塞,但是设置的queuesize=1,

每次有5个task,那就是说往队列里面塞1个,剩下4个(别较真我懂你意思)要创建新的max线程了。

结果:

  • 核心线程数:50
  • 队列:1
  • max线程:4个

因为50个核心线程在waiting中,所以队列只要一add,就会立马被消费,假设消费的这个核心线程名字是小A。

这里要细品一下:

这里已经总线程数大于核心线程数了,那么getTask()里面

// timed=true
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

那么小A干完活就会走进

//线程状态为 timedwaiting
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)

此处核心线程小A就会变成timedwaiting的状态(keepalive设置的是60s)

6. 到第12次的时候

继续往队列塞1个,创建4个max线程,max线程已经有8个了

这里 又会有一个新的核心线程小B ,会变成timedwaiting状态了

max线程们干完手上的活后,也会去调用getTask() 也会进入timedwaiting状态

因为queuesize=1,狼多肉少

7. 继续下去,那么最终会变成

max满了,线程们都在timedwaiting(keepalive设置的是60s)

新的提交就会走拒绝策略了

问题总结

其实核心与非核心对于线程池来说都是一样的,只要一旦线程数超过了核心线程数,那么线程就会走进timewaiting

把queuesize调大就好了?

这里又有一个新的注意点:

上面举例的是I/O密集型业务,queuesize不是越大越好的,

因为:

线程池新创建的线程会优先处理新请求进来的任务,而不是去处理队列里的任务,队列里的任务只能等核心线程数忙完了才能被执行,这样可能造成队列里的任务长时间等待,导致队列积压,尤其是I/O密集场景

慎用CallRunnerPolicy这个拒绝策略

一定得理解这个策略会带来什么影响,

先看下这个拒绝策略的源码

如果你提交线程池的任务即时失败也没有关系的话,用这个拒绝策略是致命的,

因为一旦超过线程池的负载后开始吞噬tomcat线程。

用future.get的方式慎用DiscardPolicy这个拒绝策略

如果需要得到线程池里的线程执行结果,使用future的方式,拒绝策略不建议使用DiscardPolicy,这种丢弃策略虽然不执行子线程的任务,

但是还是会返回future对象(其实在这种情况下我们已经不需要线程池返回的结果了),然后后续代码即使判断了future!=null也没用,

这样的话还是会走到future.get()方法,如果get方法没有设置超时时间会导致一直阻塞下去

类似下面的伪代码:

// 如果线程池已满,新的请求会被直接执行拒绝策略,此时如果拒绝策略设置的是DiscardPolicy丢弃任务,
// 则还是会返回future对象, 这样的话后续流程还是可能会走到get获取结果的逻辑
Future<String> future = executor.submit(() -> {
// 业务逻辑,比如调用第三方接口等操作
return result;
}); // 主流程调用逻辑
if(future != null) // 如果拒绝策略是DiscardPolicy还是会走到下面代码
future.get(超时时间); // 调用方阻塞等待结果返回,直到超时

推荐解决方案

1. 用动态线程池,可以动态修改coresize,maxsize,queuesize,keepalivetime

  • 对线程池的核心指标进行埋点监控,可以通过继承 ThreadPoolExecutor 然后Override掉beforeExecute,afterExecute,shutdown,shutdownNow方法,进行埋点记录到es
  • 可以埋点的数据有:

    包括线程池运行状态、核心线程数、最大线程数、任务等待数、已完成任务数、线程池异常关闭等信息
名称 含义
core_pool_size 定义的核心线程总数
max_pool_size 定义的maxpoolsize
keep_alive_time 定义的keepalivetime
current_pool_size 当前线程池总线程数
queue_wait_size 当前队列中等待处理的个数
active_count 当前run状态的线程数
completed_count 当前线程池中的每个线程处理的task数的叠加值
task_count 等于completed_count加上queue_wait_size
shutdown 当前线程池的状态是否关闭
useRate 当前线程池利用率:((active_count * 1.0 / max_pool_size) * 100)

基于以上数据,我们可以实时监控和排查定位问题

参考代码:

/**
* 自定义线程池<p>
* 1.监控线程池状态及异常关闭等情况<p>
* 2.监控线程池运行时的各项指标, 比如:任务执行时间、任务等待数、已完成任务数、任务异常信息、核心线程数、最大线程数等<p>
* author: maoyingxu
*/
public class ThreadPoolExt extends ThreadPoolExecutor{ private TimeUnit timeUnit; public ThreadPoolExt(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
this.timeUnit = unit;
} @Override
protected void beforeExecute(Thread t, Runnable r) {
monitor("ThreadPool monitor data:"); // 监控线程池运行时的各项指标
} @Override
protected void afterExecute(Runnable r, Throwable ex) {
// 记录线程池执行任务的时间
ELKLogUtils.addAppendedValue(StoredLogTag.RUNNING_DETAIL, MessageFormat.format("ThreadPool task executeTime:{0}", executeTime));
if (ex != null) { // 监控线程池中的线程执行是否异常
LogUtils.warn("unknown exception caught in ThreadPool afterExecute:", ex);
}
} @Override
public void shutdown() {
monitor("ThreadPool will be shutdown:"); // 线程池将要关闭事件,此方法会等待线程池中正在执行的任务和队列中等待的任务执行完毕再关闭
super.shutdown();
} @Override
public List<Runnable> shutdownNow() {
monitor("ThreadPool going to immediately be shutdown:"); // 线程池立即关闭事件,此方法会立即关闭线程池,但是会返回队列中等待的任务 // 记录被丢弃的任务, 目前只记录日志, 后续可根据业务场景做进一步处理
List<Runnable> dropTasks = null;
try {
dropTasks = super.shutdownNow();
ELKLogUtils.addAppendedValue(StoredLogTag.RUNNING_DETAIL, MessageFormat.format("{0}ThreadPool discard task count:{1}{2}",
System.lineSeparator(), dropTasks!=null ? dropTasks.size() : 0, System.lineSeparator()));
} catch (Exception e) {
LogUtils.addClogException("ThreadPool shutdownNow error", e);
}
return dropTasks;
} /**
* 监控线程池运行时的各项指标, 比如:任务等待数、任务异常信息、已完成任务数、核心线程数、最大线程数等
* @param title
*/
private void monitor(String title){
try {
// 线程池监控信息记录, 这里需要注意写ES的时机,尤其是多个子线程的日志合并到主流程的记录方式
String threadPoolMonitor = MessageFormat.format(
"{0}{1}core pool size:{2}, current pool size:{3}, queue wait size:{4}, active count:{5}, completed task count:{6}, " +
"task count:{7}, largest pool size:{8}, max pool size:{9}, keep alive time:{10}, is shutdown:{11}, is terminated:{12}, " +
"thread name:{13}{14}",
System.lineSeparator(), title, this.getCorePoolSize(), this.getPoolSize(),
this.getQueue().size(), this.getActiveCount(), this.getCompletedTaskCount(), this.getTaskCount(), this.getLargestPoolSize(),
this.getMaximumPoolSize(), this.getKeepAliveTime(timeUnit != null ? timeUnit : TimeUnit.SECONDS), this.isShutdown(),
this.isTerminated(), Thread.currentThread().getName(), System.lineSeparator());
ELKLogUtils.addAppendedValue(StoredLogTag.RUNNING_DETAIL, threadPoolMonitor);
LogUtils.info(title, threadPoolMonitor); ELKLogUtils.addFieldValue(APPIndexedLogTag.THREAD_POOL_USE_RATE, useRate); // ES埋点线程池使用率, useRate = (getActiveCount()/getMaximumPoolSize())*100
Cat.logEvent(key, String.valueOf(useRate)); // 报警设置
} catch (Exception e) {
LogUtils.addClogException("ThreadPool monitor error", e);
}
} }

2. 重写线程池拒绝策略, 拒绝策略主要参考了 Dubbo的线程池拒绝策略

public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {

    // 省略部分代码

    @Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
String msg = String.format("Thread pool is EXHAUSTED!" +
" Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: "
+ "%d)," +
" Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(),
e.getLargestPoolSize(),
e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
url.getProtocol(), url.getIp(), url.getPort());
logger.warn(msg); // 记录最大负载情况下线程池的核心线程数,活跃数,最大线程数等参数
dumpJStack(); // 记录线程堆栈信息包括锁争用信息
throw new RejectedExecutionException(msg);
} private void dumpJStack() {
long now = System.currentTimeMillis(); //dump every 10 minutes 每隔10分钟记录一次
if (now - lastPrintTime < TEN_MINUTES_MILLS) {
return;
} if (!guard.tryAcquire()) { // 加锁访问
return;
} ExecutorService pool = Executors.newSingleThreadExecutor(); // 这里单独开启一个新的线程去执行(阿里的Java开发规范不允许直接调用Executors.newSingleThreadExecutor, 估计dubbo那时候还没出开发规范...)
pool.execute(() -> {
String dumpPath = url.getParameter(DUMP_DIRECTORY, System.getProperty("user.home")); SimpleDateFormat sdf; String os = System.getProperty(OS_NAME_KEY).toLowerCase(); // window system don't support ":" in file name
if (os.contains(OS_WIN_PREFIX)) {
sdf = new SimpleDateFormat(WIN_DATETIME_FORMAT);
} else {
sdf = new SimpleDateFormat(DEFAULT_DATETIME_FORMAT);
} String dateStr = sdf.format(new Date());
//try-with-resources
try (FileOutputStream jStackStream = new FileOutputStream(
new File(dumpPath, "Dubbo_JStack.log" + "." + dateStr))) {
JVMUtil.jstack(jStackStream);
} catch (Throwable t) {
logger.error("dump jStack error", t);
} finally {
guard.release();
}
lastPrintTime = System.currentTimeMillis();
});
//must shutdown thread pool ,if not will lead to OOM
pool.shutdown(); } }

以上理解如果有误,欢迎大佬指正。

参考资料:

  • Dubbo线程池拒绝策略: org.apache.dubbo.common.threadpool.support.AbortPolicyWithReport.java
  • 《Java并发编程实战》

java线程池实践的更多相关文章

  1. Java线程池实现原理及其在美团业务中的实践

    本文转载自Java线程池实现原理及其在美团业务中的实践 导语 随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流.使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器.J.U.C提供 ...

  2. Java线程池实现原理及其在美团业务中的实践(转)

    转自美团技术团队:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html 随着计算机行业的飞速发展,摩尔定律逐 ...

  3. JAVA线程池应用的DEMO

    在做很多高并发应用的时候,单线程的瓶颈已经满足不了我们的需求,此时使用多线程来提高处理速度已经是比较常规的方案了.在使用多线程的时候,我们可以使用线程池来管理我们的线程,至于使用线程池的优点就不多说了 ...

  4. JAVA 线程池之Callable返回结果

    本文介绍如何向线程池提交任务,并获得任务的执行结果.然后模拟 线程池中的线程在执行任务的过程中抛出异常时,该如何处理. 一,执行具体任务的线程类 要想 获得 线程的执行结果,需实现Callable接口 ...

  5. Java 线程池的原理与实现 (转)

        最近在学习线程池.内存控制等关于提高程序运行性能方面的编程技术,在网上看到有一哥们写得不错,故和大家一起分享. [分享]Java 线程池的原理与实现 这几天主要是狂看源程序,在弥补了一些以前知 ...

  6. 【转载】深度解读 java 线程池设计思想及源码实现

    总览 开篇来一些废话.下图是 java 线程池几个相关类的继承结构: 先简单说说这个继承结构,Executor 位于最顶层,也是最简单的,就一个 execute(Runnable runnable) ...

  7. Java并发指南12:深度解读 java 线程池设计思想及源码实现

    ​深度解读 java 线程池设计思想及源码实现 转自 https://javadoop.com/2017/09/05/java-thread-pool/hmsr=toutiao.io&utm_ ...

  8. Java线程池的了解使用—筑基篇

    前言 Java中的线程池是一个很重要的概念,它的应用场景十分广泛,可以被广泛的用于高并发的处理场景.J.U.C提供的线程池:ThreadPoolExecutor类,可以帮助我们管理线程并方便地并行执行 ...

  9. 一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结

    一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结 这是一个十分严重的线上问题 自从最近的某年某月某天起,线上服务开始变得不那么稳定(软病).在高峰期,时常有几台机器的 ...

随机推荐

  1. Spring中各种扩展原理及容器创建原理

    一.BeanFactoryPostProcessor BeanFactory的后置处理器:在BeanFactory标准初始化之后调用,来定制和修改BeanFactory的内容:所有的bean定义已经保 ...

  2. 超详细Linux新手快速入门(一)——Linux的介绍安装以及虚拟机的介绍安装

    一.Linux的介绍 1.Linux和Windows的比较  Linux是一款操作系统,其性能稳定,因其防火墙组件高效安全.简单易配置,所以获得了追求速度和安全的一些企业和人群的青睐.与我们日常所熟知 ...

  3. P1014_Cantor表 (JAVA语言)

    题目描述 现代数学的著名证明之一是Georg Cantor证明了有理数是可枚举的.他是用下面这一张表来证明这一命题的: 1/11/1 , 1/21/2 , 1/31/3 , 1/41/4, 1/51/ ...

  4. IaaS, PaaS和SaaS的区别

    从小型企业到全球企业,云都是一个非常热门的话题,它是一个非常广泛的概念,涵盖了很多在线领域. 无论是应用程序还是基础架构部署,当您开始考虑将业务转移到云时,了解各种云服务的差异和优势比以往任何时候都更 ...

  5. tips 【总结】

    需求 移入a标签把对应的详情显示出来并且根据位置判断,当前详情是否超出父级可视区范围,如果超出就定位的距离方向应该正好在父级可视区范围内 需求分析: 需要用到: offsetLeft   获取外边框到 ...

  6. OpenGL 绘制你的 github skyline 模型

    前言 好久没更新博客了,上一篇文章还是在去年6月份,乍一看居然快要过去一年了,不时还能看到粉丝数和排名在涨,可是却一直没有内容更新,怪不好意思的- -(主要是一直没想好要写些什么,中间也有过一些想法, ...

  7. RepVGG

    RepVGG: Making VGG-style ConvNets Great Again 作者:elfin   资料来源:RepVGG论文解析 目录 1.摘要 2.背景介绍 3.相关工作 3.1 单 ...

  8. java例题_24 逆向输入数字

    1 /*24 [程序 24 根据输入求输出] 2 题目:给一个不多于 5 位的正整数,要求:一.求它是几位数,二.逆序打印出各位数字. 3 */ 4 5 /*分析 6 * 首先从键盘得到一个正整数,不 ...

  9. 全面了解Vue3的 reactive 和相关函数

    Vue3的 reactive 怎么用,原理是什么,官网上和reactive相关的那些函数又都是做什么用处的?这里会一一解答. ES6的Proxy Proxy 是 ES6 提供的一个可以拦截对象基础操作 ...

  10. [图论]最优布线问题:prim

    最优布线问题 目录 最优布线问题 Description Input Output Sample Input Sample Output Hint 解析 代码 Description 学校有n台计算机 ...