线程池大家都很熟悉,无论是平时的业务开发还是框架中间件都会用到,大部分都是基于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. FreeBSD 12.2 阿里云镜像使用说明

    目前直接从阿里云 12.1 升级 12.2 会导致错误.镜像非本人制作.FreeBSD 12.2 阿里云镜像使用说明镜像下载地址: http://t.cn/A6taB5jO修改内容:对 /usr/sr ...

  2. JAVA使用Collator对中文排序

    首先创建一个集合 public static List<String> init() { List<String> list = new ArrayList<String ...

  3. vue全家桶和react全家桶

    vue全家桶:vue  +  vuex (状态管理)  + vue-router (路由) + vue-resource +axios +elementui react全家桶 : react + re ...

  4. P1579_哥德巴赫猜想(JAVA语言)

    题目背景 1742年6月7日哥德巴赫写信给当时的大数学家欧拉,正式提出了以下的猜想:任何一个大于9的奇数都可以表示成3个质数之和.质数是指除了1和本身之外没有其他约数的数,如2和11都是质数,而6不是 ...

  5. 从本质彻底精通Git——4个模型1个周期1个史观1个工作流

    一.什么是Git? Git是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目. Git是Linus Torvalds为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软 ...

  6. 微信小程序实现搜索关键词高亮

    目录 1,前言 2,思路 3,代码逻辑 1,前言 项目中碰到一个需求,搜索数据并且关键词要高亮显示,接到需求,马上开干.先上效果图.源码已经做成了小程序代码片段,放入了GitHub了,文章底部有源码链 ...

  7. HarmonyOS三方件开发指南(15)-LoadingView功能介绍

    目录: 1. LoadingView组件功能介绍2. Lottie使用方法3. Lottie开发实现4.<HarmonyOS三方件开发指南>系列文章合集 1. LoadingView组件功 ...

  8. 使用 Android Studio 的日志工具 Log

    •Log简介 Android中的日志工具类是Log,这个类中提供了5个方法来供我们打印日志: 1. $Log.v()$ 用于打印那些最为琐碎的,意义最小的日志信息 对应级别 verbose,是 And ...

  9. 热更新语言--lua学习笔记

    一.lua安装和编程环境搭建 lua语言可以在官网:http://luadist.org/下载安装包安装,编程IDE之前学习使用的是SciTE(https://www.cnblogs.com/movi ...

  10. TypeError: 'list' object cannot be interpreted as an integer Python常见错误

    想要通过索引来迭代一个list或者string的元素, 这需要调用 range() 函数.要记得返回len 值而不是返回这个列表.