面试官问:你做过什么Java线程池实践,我写了一篇博客给他看~

线程池大家都## 标题很熟悉,无论是平时的业务开发还是框架中间件都会用到,大部分都是基于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;}
//加入Java开发交流君样:756584822一起吹水聊天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状态,
只要队列一有活就会被唤醒去干活。

  1. 到第11次的时候
    好家伙,到这步骤的时候 ,核心线程数已满,那么就往队列里面塞,但是设置的queuesize=1,
    每次有5个task,那就是说往队列里面塞1个,剩下4个(别较真我懂你意思)要创建新的max线程了。

结果:

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

这里要细品一下:

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

// timed=trueboolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

那么小A干完活就会走进

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

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

  1. 到第12次的时候
    继续往队列塞1个,创建4个max线程,max线程已经有8个了

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

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

因为queuesize=1,狼多肉少

  1. 继续下去,那么最终会变成
    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
    可以埋点的数据有:
    包括线程池运行状态、核心线程数、最大线程数、任务等待数、已完成任务数、线程池异常关闭等信息

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

参考代码:

/*** 自定义线程池<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;} //加入Java开发交流君样:756584822一起吹水聊天@Overrideprotected void beforeExecute(Thread t, Runnable r) {monitor("ThreadPool monitor data:"); // 监控线程池运行时的各项指标}@Overrideprotected 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);}}@Overridepublic void shutdown() {monitor("ThreadPool will be shutdown:"); // 线程池将要关闭事件,此方法会等待线程池中正在执行的任务和队列中等待的任务执行完毕再关闭super.shutdown();}@Overridepublic 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);}//加入Java开发交流君样:756584822一起吹水聊天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())*100Cat.logEvent(key, String.valueOf(useRate)); // 报警设置} catch (Exception e) {LogUtils.addClogException("ThreadPool monitor error", e);}}}
  1. 重写线程池拒绝策略, 拒绝策略主要参考了 Dubbo的线程池拒绝策略
public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {// 省略部分代码@Overridepublic 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;}//加入Java开发交流君样:756584822一起吹水聊天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 nameif (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-resourcestry (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 OOMpool.shutdown();}}

最后,祝大家早日学有所成,拿到满意offer

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/299698.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Blog.Idp开源】支持在线密码找回

&#xff08;一个做认证平台&#xff0c;必须会遇到的一个问题&#xff09;BCVP框架&#xff0c;是基于:ASP.NETCore5.0VUE.jsIdentityServer4等核心技术&#xff0c;实现的前后端分离与动态认证鉴权一体化平台。01密码找回认证中心绕不开的话题Architecture Design.无论你是自…

我的狗丢了,所以我能加你微信吗? | 今日最佳

全世界只有3.14 % 的人关注了青少年数学之旅&#xff08;图源网络&#xff0c;侵权删&#xff09;

将VC++6.0的代码迁移到VS2005常见问题总结(Window核心编程第五版以前代码在VS2005无法编译的解决方案)...

额喜新厌旧是男人的通病吧&#xff0c;可是呢VS2005的界面看着的确比VC6.0看着舒服&#xff0c;而且也算用习惯了吧。可是网上现在大部分C/C的代码还是用VC6.0的。这为我们这些菜鸟的学习之路增添了不少障碍&#xff0c;可能有很多朋友在这一步就放弃了吧或者抹黑走下去&#x…

被问到了!为什么一定要使用分布式,内行啊

一、为什么要使用分布式 如果需求要测试 4000 虚拟用户数&#xff0c;而本机只能支持1000 虚拟用户&#xff0c;如果测试结果有可能是电脑的问题&#xff0c;而不是服务器的问题&#xff0c;所以需要把其他虚拟用户分配到多台电脑上 把虚拟用户数分配到其他电脑上面去执行&am…

设计模式之享元

享元模式介绍享元模式主要在于共享通用对象&#xff0c;减少内存的使用&#xff0c;提升系统的访问效率。而这部分共享对象通常比较耗费内存或者需要查询大量接口或者使用数据库资源&#xff0c;因此统一抽离作为共享对象使用。在使用此模式过程中&#xff0c;需要使用享元工厂…

真正拉开人与人之间的差距是什么?

全世界只有3.14 % 的人关注了青少年数学之旅身边总有些人看上去很轻松&#xff0c;不仅在工作中游刃有余&#xff0c;还知识渊博&#xff0c;对各种事情有自己的思考。这样的人一定是天生的学霸吧。其实学习不一定要在教室里从一本书的第一页开始看&#xff0c;学习可以很轻松。…

[导入]【翻译】WF从入门到精通(第八章):调用外部方法及工作流

摘要: 学习完本章&#xff0c;你将掌握&#xff1a; 1.创建并调用你的工作流外部的本地数据服务 2.理解怎样使用接口来为宿主进程和你的工作流之间进行通信。 3.使用设计的外部方法在你的工作流和宿主应用程序之间传输数据。 4.在一个正执行的工作流中调用其它工作流 阅读全文…

(译)Windsor入门教程---第三部分 编写第一个Installer

原文&#xff1a;http://docs.castleproject.org/Windsor.Windsor-tutorial-ASP-NET-MVC-3-application-To-be-Seen.ashx 简介 在第二部分我们创建了控制器工厂。现在我们要把我们的控制器交给Windsor来管理。 Installer Windsor有一个专门的类installer.cs&#xff0c;用来向容…

在 ASP.NET Core 中使用 Serilog 使用 Fluentd 将日志写入 Elasticsearch

在 ASP.NET Core 中使用 Serilog 使用 Fluentd 将日志写入 Elasticsearch原文来自&#xff1a;https://andrewlock.net/writing-logs-to-elasticsearch-with-fluentd-using-serilog-in-asp-net-core/对于在 Kubernetes 中运行的应用程序&#xff0c;将日志消息存储在一个中心位…

2021年度最全面JVM虚拟机,类加载过程与类加载器

前言 类装载器子系统是JVM中非常重要的部分&#xff0c;是学习JVM绕不开的一关。 一般来说&#xff0c;Java 类的虚拟机使用 Java 方式如下&#xff1a; Java 源程序&#xff08;.java 文件&#xff09;在经过 Java 编译器编译之后就被转换成 Java 字节代码&#xff08;.class …

做生意最重要的诚信呢??? | 今日最佳

全世界只有3.14 % 的人关注了青少年数学之旅&#xff08;图源网络&#xff0c;侵权删&#xff09;

面试避坑手册之 Java字节流和字符流总结IO流!

从接收输入值说起 在日常的开发应用中&#xff0c;有时候需要直接接收外部设备如键盘等的输入值&#xff0c;而对于这种数据的接收方式&#xff0c;我们一般有三种方法&#xff1a;字节流读取&#xff0c;字符流读取&#xff0c;Scanner 工具类读取。 字节流读取 直接看一个…

这家AI公司用面具破解中国人脸识别系统!微信、支付宝、火车站无一幸免

全世界只有3.14 % 的人关注了青少年数学之旅据外媒报道&#xff0c;一家人工智能公司Kneron用一个特制的3D面具&#xff0c;成功欺骗了包括支付宝和微信在内的诸多人脸识别支付系统&#xff0c;完成了购物支付程序。他们用同样的方式甚至进入了中国的火车站。现如今&#xff0c…

coolite TreeNode NodeClick传id到后台的方法

重点如下&#xff1a; 1 <AjaxEvents>2 <Click OnEvent"PanelTree_Click" >3 <EventMask ShowMask"true" Msg"正在执行,请稍后" />4 <E…

使用JavaScript实现页面选项自动添加行以及删除行 javaweb

2019独角兽企业重金招聘Python工程师标准>>> <% page language"java" import"java.util.*" pageEncoding"UTF-8"%> <% String path request.getContextPath(); String basePath request.getScheme()"://"reques…

C#基础知识之base、this、new、override、abstract梳理

一、Base关键词的几种用法base重要用于OOP的多态上&#xff0c;base 关键字用于在派生类中实现对基类公有或者受保护成员的访问&#xff0c;但是只局限在构造函数、实例方法和实例属性访问器中1、base调用基类构造函数using System;namespace BaseDemo {class Program{static v…

班主任老师推荐这些优质的教育号,建议家长们多阅读!

全世界只有3.14 % 的人关注了青少年数学之旅推荐几个教育类学习号让孩子少走弯路&#xff0c;为孩子成长保驾护航&#xff01;长按二维码&#xff0c;选择【识别图中二维码】关注理想父母 lixiangfumu&#xff08;长按二维码识别关注&#xff09;关注理由&#xff1a;面向家长…

学妹问我Java枚举类与注解,我直接用这个搞定她!

很多人问我学妹长什么样&#xff0c;不多说 上图吧&#xff01; 学妹问我Java枚举类与注解&#xff0c;我直接一篇文章搞定&#xff01;一、枚举类① 自定义枚举类② enum关键字定义枚举类③ enum 枚举类的方法④ enum 枚举类实现接口二、注解① 生成文档相关注解②注解在编译…

设计模式之代理

代理模式介绍啥是代理模式&#xff1f;代理模式 是一种结构型设计模式&#xff0c;让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问&#xff0c;并允许在将请求提交给对象前后进行一些处理。其实说通俗点&#xff0c;就好比我们平时生活中的购买机票&#xff…

轮子,辛苦你了。 | 今日最佳

全世界只有3.14 % 的人关注了青少年数学之旅&#xff08;图源帅哥李坏的朋友圈&#xff0c;侵权删&#xff09;