1. 事件背景

最近公司开发了一个新功能:统计有关联课程的学习信息,数据是每天晚上12点开始统计。由于数据量有点大,所以每次统计当天的任务需要差不多10个小时。运营人员需要第二天就看到数据,但是每天10点后才能看到确实有点不合理。于是组长让我把代码统计任务程序时间缩短一下。

2.案例还原演示

下面是我通过简单代码大概模拟的统计代码(当然和真实代码不是一样的),主要有2个核心的方法:一个是获取所有的课程信息,一个是根据课程Id获取有关联课程的统计信息(课程单元数量和所有关联课程学习总人数)。

为了方便演示获取所有的课程信息和根据课程Id获取有关联课程的统计信息 没有对接数据库,而是采用来写死的数据。

其中 获取所有的课程信息 中课程信息为20个,为了模拟真实效果该方法获取需要耗时 20秒,根据课程Id获取有关联课程的统计信息每调用一次需要2秒。

课程实体类

public class Course {private Long id;private String name;//省略getter and setter
}

课程服务类包含上面讲的2个核心方法。

public class CourseService {/*** 获取所有的课程* @return*/public List<Course>  findAll(){List list = new ArrayList();for (int i = 0; i < 20; i++) {list.add(new Course((long) i,"course"+i));}try {Thread.sleep(1000*1L);} catch (InterruptedException e) {e.printStackTrace();}return list;}/*** 获取有关联课程的统计信息*/public void getClassStatisticsInfo(Course course){getUnitNum(course.getId());getLearnNum(course.getId());}/*** 获取课程的单元数量*/private void getUnitNum(Long id) {try {Thread.sleep(1000*1L);} catch (InterruptedException e) {e.printStackTrace();}}/*** 获取所有关联课程学习总人数*/private void getLearnNum(Long id) {try {Thread.sleep(1000*1L);} catch (InterruptedException e) {e.printStackTrace();}}
}

普通单线程的处理方式,这个是没有优化前代码模拟,首先是获取所有的课程信息,然后遍历每个课程信息根据课程ID 获取有关联课程的统计信息,任务是一个一个执行的所以比较耗时。

普通单线程的处理方式具体代码如下:

/*** 普通单线成任务*/private void ordinaryTask() {long startTime = System.currentTimeMillis();//获取数据库中数据CourseService courseStatisticsService = new CourseService();List<Course> list = courseStatisticsService.findAll();//处理任务for (Course course : list) {courseStatisticsService.getClassStatisticsInfo(course);System.out.println("课程:" + course.getName() + "处理完成!完成时间:"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));}long endTime = System.currentTimeMillis();System.out.println("任务耗时:"+(endTime - startTime));}}

测试普通单线程的处理方式执行时间测试类:

public class Application {public static void main(String[] args) {Application application = new Application();application.ordinaryTask();}

普通单线程的处理方式日志如下:

课程:course0处理完成!完成时间:2020-04-18 17:54:04
课程:course1处理完成!完成时间:2020-04-18 17:54:06
课程:course2处理完成!完成时间:2020-04-18 17:54:08
课程:course3处理完成!完成时间:2020-04-18 17:54:10
课程:course4处理完成!完成时间:2020-04-18 17:54:12
课程:course5处理完成!完成时间:2020-04-18 17:54:14
课程:course6处理完成!完成时间:2020-04-18 17:54:16
课程:course7处理完成!完成时间:2020-04-18 17:54:18
课程:course8处理完成!完成时间:2020-04-18 17:54:20
课程:course9处理完成!完成时间:2020-04-18 17:54:22
课程:course10处理完成!完成时间:2020-04-18 17:54:24
课程:course11处理完成!完成时间:2020-04-18 17:54:26
课程:course12处理完成!完成时间:2020-04-18 17:54:28
课程:course13处理完成!完成时间:2020-04-18 17:54:30
课程:course14处理完成!完成时间:2020-04-18 17:54:32
课程:course15处理完成!完成时间:2020-04-18 17:54:34
课程:course16处理完成!完成时间:2020-04-18 17:54:36
课程:course17处理完成!完成时间:2020-04-18 17:54:38
课程:course18处理完成!完成时间:2020-04-18 17:54:40
课程:course19处理完成!完成时间:2020-04-18 17:54:42
任务耗时:41225

普通单线程的处理方式执行时间是 41225 毫秒 转换成秒就是:41.225秒。

3.解决方案

在解决前首先第一步要做的是定位具体那个方法比较耗时并分析原因。分析可以从以下几个方面入手

1.查询sql 是否过于耗时
2.是否存在临时对象的大量使用或者内存泄漏

如果是查询sql 问题那么需要通过优化sql 来提高查询效率,如果是程序问题应该避免临时对象的大量使用,对于程序中对象已不再使用,但对它的引用还保留着,这些对象就造成了内存泄漏。所以要及时已经清理对象的引用。

处理的第一步通过日志查看每个方法运行时间,然后查看具体sql 执行发现好多查询条件字段并没有索引,所以造成查询时间过长。在对表某些高频率字段添加索引后时间虽然有所提升,但是整体运行时间还是很长。

为了快速提升程序执行效率,我没有看程序代码的问题。而是采用使用多线程的方式解决,就是多个线程同时执行这个统计任务来提升程序的运行时间。

具体处理思路:假设我们有4个需要执行的任务,每个任务执行的时间为1 秒,如果我们单线程执行需要 4秒,如果有2个线程同时执行的话就可以将执行时间缩短为2秒。通俗点讲就是:4件事情交给一个人做需要4秒,那么交给2个人同时处理就需要2秒。具体如下图所示:

处理方式是通过 Executors.newFixedThreadPool(threadNum)来创建一个固定数量的线程池,将任务遍历通过线程池的线程来执行。

创建一个固定数量为 5 个的线程池,可以理解成任务可以在同一时间处理5个,比单线程的方式可以缩短5倍的时间。

Executors线程池的处理方式代码如下:

    public void executors(){long startTime = System.currentTimeMillis();//获取数据库中数据CourseService courseStatisticsService = new CourseService();List<Course> list = courseStatisticsService.findAll();//常见固定数量的线程池ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);//taskTimes 用于记录执行的次数 当任务执行完毕是统计执行时间List<String> taskTimes = new ArrayList();//处理任务for (Course course : list) {fixedThreadPool.execute(() -> {courseStatisticsService.getClassStatisticsInfo(course);System.out.println("课程:" + course.getName() + "处理完成!完成时间:"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));if(taskTimes.size() == list.size()-1){Long endTime = System.currentTimeMillis();System.out.println("任务耗时:"+(endTime - startTime));}taskTimes.add("1");});}fixedThreadPool.shutdown();}

Executors线程池的处理方式日志如下:

课程:course0处理完成!完成时间:2020-04-18 17:55:42
课程:course4处理完成!完成时间:2020-04-18 17:55:42
课程:course3处理完成!完成时间:2020-04-18 17:55:42
课程:course1处理完成!完成时间:2020-04-18 17:55:42
课程:course2处理完成!完成时间:2020-04-18 17:55:42
课程:course5处理完成!完成时间:2020-04-18 17:55:44
课程:course7处理完成!完成时间:2020-04-18 17:55:44
课程:course6处理完成!完成时间:2020-04-18 17:55:44
课程:course9处理完成!完成时间:2020-04-18 17:55:44
课程:course8处理完成!完成时间:2020-04-18 17:55:44
课程:course10处理完成!完成时间:2020-04-18 17:55:46
课程:course13处理完成!完成时间:2020-04-18 17:55:46
课程:course14处理完成!完成时间:2020-04-18 17:55:46
课程:course12处理完成!完成时间:2020-04-18 17:55:46
课程:course11处理完成!完成时间:2020-04-18 17:55:46
课程:course15处理完成!完成时间:2020-04-18 17:55:48
课程:course19处理完成!完成时间:2020-04-18 17:55:48
课程:course17处理完成!完成时间:2020-04-18 17:55:48
课程:course18处理完成!完成时间:2020-04-18 17:55:48
课程:course16处理完成!完成时间:2020-04-18 17:55:48
任务耗时:9183

Executors 线程池的处理方式执行时间是 9183 毫秒 转换成秒就是:9.183秒。虽然没有准确的提升5倍,但是和预期的差不多。

4.解决方案存在问题

翻阅阿里巴巴 Java手册,其中有有一条明确规定线程池不允许使用 Executors 去创建,因为FixedThreadPool 可能导致 OOM,具体内容如下:

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这
样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1)
FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为
Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2) CachedThreadPool: 允许的创建线程数量为
Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

在使用ThreadPoolExecutor 改造之前先介绍一下 ThreadPoolExecutor 构造方法7个参数:

  • corePoolSize:线程池中的核心线程数
  • maximumPoolSize:线程池中最大线程数
  • keepAliveTime:闲置超时时间
  • unit:和 keepAliveTime 配合使用标明超时时间的单位
  • workQueue:线程池的任务队列
  • threadFactory:创建新线程的线程工厂
  • rejectedExecutionHandler:线程池任务队列超过最大值之后的拒绝策略

线程池任务队列超过最大值之后的拒绝策略如下:

  • new ThreadPoolExecutor.DiscardPolicy():超出的任务直接丢弃,不进行任何处理。
  • new ThreadPoolExecutor.DiscardOldestPolicy():执行最新的任务,丢弃掉未执行的老任务。
  • new ThreadPoolExecutor.AbortPolicy():抛出 RejectedExecutionException
    异常。
  • new ThreadPoolExecutor.CallerRunsPolicy():超出的任务由主线程(调用方)来执行处理。

下面是我通过ThreadPoolExecutor 创建的线程池的代码:

任务队列为15个,线程池任务队列超过最大值之后的拒绝策略采用 new ThreadPoolExecutor.AbortPolicy()。也就是报异常。

    public void threadPoolExecutor() {long startTime = System.currentTimeMillis();//获取数据库中数据CourseService courseStatisticsService = new CourseService();List<Course> list = courseStatisticsService.findAll();//常见固定数量的线程池 和 Executors.newFixedThreadPool 一样区别是队列数量不一样ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 5,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(15),Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());//taskTimes 用于记录执行的次数 当任务执行完毕是统计执行时间List<String> taskTimes = new ArrayList();for (Course course : list) {threadPool.execute(() -> {courseStatisticsService.getClassStatisticsInfo(course);System.out.println("课程:" + course.getName() + "处理完成!完成时间:" +  LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));if(taskTimes.size() == list.size()-1){Long endTime = System.currentTimeMillis();System.out.println("任务耗时:"+(endTime - startTime));}taskTimes.add("1");});}threadPool.shutdown();}
课程:course0处理完成!完成时间:2020-04-18 17:56:33
课程:course3处理完成!完成时间:2020-04-18 17:56:33
课程:course4处理完成!完成时间:2020-04-18 17:56:33
课程:course1处理完成!完成时间:2020-04-18 17:56:33
课程:course2处理完成!完成时间:2020-04-18 17:56:33
课程:course9处理完成!完成时间:2020-04-18 17:56:35
课程:course7处理完成!完成时间:2020-04-18 17:56:35
课程:course6处理完成!完成时间:2020-04-18 17:56:35
课程:course5处理完成!完成时间:2020-04-18 17:56:35
课程:course8处理完成!完成时间:2020-04-18 17:56:35
课程:course12处理完成!完成时间:2020-04-18 17:56:37
课程:course11处理完成!完成时间:2020-04-18 17:56:37
课程:course13处理完成!完成时间:2020-04-18 17:56:37
课程:course14处理完成!完成时间:2020-04-18 17:56:37
课程:course10处理完成!完成时间:2020-04-18 17:56:37
课程:course18处理完成!完成时间:2020-04-18 17:56:39
课程:course19处理完成!完成时间:2020-04-18 17:56:39
课程:course15处理完成!完成时间:2020-04-18 17:56:39
课程:course16处理完成!完成时间:2020-04-18 17:56:39
课程:course17处理完成!完成时间:2020-04-18 17:56:39
任务耗时:9158

通过日志我们可以发现ThreadPoolExecutor 和 Executors线程池的处理方式耗时时间差不多。这个也和预期一样。

我将CourseService 中获取所有课程的数据提升到 21 个,因为我们采取的是new ThreadPoolExecutor.AbortPolicy()。所以超出的一个任务不会被处理,并报RejectedExecutionException 异常。

具体代码如下:

 /*** 获取所有的课程* @return*/public List<Course>  findAll(){List list = new ArrayList();for (int i = 0; i < 21; i++) {list.add(new Course((long) i,"course"+i));}try {Thread.sleep(1000*1L);} catch (InterruptedException e) {e.printStackTrace();}return list;}

执行统计程序会报了一个 RejectedExecutionException异常,并且任务只能处理20个。具体报错日志信息如下:

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task cn.zhuoqianmingyue.statistics.Application$$Lambda$1/1329552164@10f87f48 rejected from java.util.concurrent.ThreadPoolExecutor@b4c966a[Running, pool size = 5, active threads = 5, queued tasks = 15, completed tasks = 0]at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)at cn.zhuoqianmingyue.statistics.Application.threadPoolExecutor(Application.java:75)at cn.zhuoqianmingyue.statistics.Application.main(Application.java:15)
课程:course4处理完成!完成时间:2020-04-18 18:22:49
课程:course0处理完成!完成时间:2020-04-18 18:22:49
课程:course1处理完成!完成时间:2020-04-18 18:22:49
课程:course2处理完成!完成时间:2020-04-18 18:22:49
课程:course3处理完成!完成时间:2020-04-18 18:22:49
课程:course7处理完成!完成时间:2020-04-18 18:22:51
课程:course9处理完成!完成时间:2020-04-18 18:22:51
课程:course8处理完成!完成时间:2020-04-18 18:22:51
课程:course5处理完成!完成时间:2020-04-18 18:22:51
课程:course6处理完成!完成时间:2020-04-18 18:22:51
课程:course12处理完成!完成时间:2020-04-18 18:22:53
课程:course10处理完成!完成时间:2020-04-18 18:22:53
课程:course13处理完成!完成时间:2020-04-18 18:22:53
课程:course11处理完成!完成时间:2020-04-18 18:22:53
课程:course14处理完成!完成时间:2020-04-18 18:22:53
课程:course19处理完成!完成时间:2020-04-18 18:22:55
课程:course18处理完成!完成时间:2020-04-18 18:22:55
课程:course16处理完成!完成时间:2020-04-18 18:22:55
课程:course17处理完成!完成时间:2020-04-18 18:22:55
课程:course15处理完成!完成时间:2020-04-18 18:22:55

我通过Executors 方式处理 21 个课程信息并没有报错,为什么 FixedThreadPool 方式没有报错呢?上面我们说到 阿里Java 手册中说到:
FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度Integer.MAX_VALUE,而我通过 ThreadPoolExecutor 方式创建的队列长度为 15个。

因为我们的核心线程数为5 最大线程数也是5 ,队列为15,所以可以处理的线程数为 20,当我们增加到 21 个后任务队列里面放不下了,就采取报错的拒绝策略。

我通过参看FixedThreadPool 验证了确实和阿里Java 手册所说一样它的任务队列的长度是:Integer.MAX_VALUE

进入newFixedThreadPool 方法如下所示newFixedThreadPool 采用了LinkedBlockingQueue 队列。

    public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}

在进入 LinkedBlockingQueue 构造方法中我们可以看到其将队列的数量设置为 Integer.MAX_VALUE 个长度。

    public LinkedBlockingQueue() {this(Integer.MAX_VALUE);}

接下来你可能还有疑问,为什么任务队列超了就要执行拒绝策略。线程池的原理是什么样的? 请您继续往下看。

5.问题引发原理深究

接下来我将通过通俗易懂的方式带你快速带你了解线程池原理。假设一个电影院有 4 个放映室,每个放映室有10个位置,如果同时放映 4 场电影就可以卖 40 张票。同时还有一个备用的放映室(如果人流程大时使用)。

现在外面下雨了,有人想买电影票想在休息室等待看电影同时还能避雨的话怎么处理呢?

商家通过在搞个等待室,一个等待室可以坐10个人。这样的话可以一次卖50张票。40个人看电影,10个在等待室等待下一场电影。

如果一次来了60个人来买票的话可以启动备用放映室,这样一次可以卖60张票,50个人看电影,10个人等待看下一场。

如果同时来70个人的话,那么商家只能跟多出的10个人说:不好意思我们现在电影院的位置都坐满了,等一会您在来吧。

案例中的例子于线程池对应的概念如下:

  • 线程池 == 电影院
  • 放映室 == 核心线程数
  • 备用放映室 + 放映室 == 最大线程数
  • 等待室 == 队列的数量
  • 商家跟多余10个人说等会再来 == 线程池任务队列超过最大值之后的拒绝策略

到这里你是不是有种恍然大明白的感觉,如果你想验证我的内容是否正确,可以具体参读 Executors 和 ThreadPoolExecutor 源码。由于篇幅有限我这里不在过多阐述。

6.小结

通过 Executors 或者 ThreadPoolExecutor 方式来创建线程池来提供任务的执行时间,Executors 相对于 ThreadPoolExecutor 使用方式上简单了许多,那是因为 Executors 是在ThreadPoolExecutor基础之上做了一些封装。

需要注意的是:使用 Executors 方式可能会堆积大量的请求,从而导致 OOM。 如果说你处理的任务量不大,也可以使用Executors 但是还是比较推荐ThreadPoolExecutor的方式。

后台定时统计任务太耗时如何优化一下相关推荐

  1. 统计信息:SQL执行优化之密钥

    SQL 执行的指导思想是什么? SQL 执行计划的正确依赖选择依赖于什么?统计信息为什么在 SQL 执行中起到关键性的作用?如何才能自动化收集统计信息?让 一起了解 SQL 执行优化的核心底座. 统计 ...

  2. jsp中java代码无效_来杯咖啡,教你如何优雅的在java中统计代码块耗时

    推荐阅读: Sping源码+Redis+Nginx+MySQL等七篇实战技术文档,阿里大佬推荐 阿里内部:2020年全技术栈文档+PPT分享,(万粉总结,回馈粉丝) 在我们的实际开发中,多多少少会遇到 ...

  3. 如何优雅的在java中统计代码块耗时

    在我们的实际开发中,多多少少会遇到统计一段代码片段的耗时的情况,我们一般的写法如下 long start = System.currentTimeMillis(); try {// .... 具体的代 ...

  4. 【探花交友DAY 11】定时统计任务和自动内容审核

    1. 用户的冻结与解冻 用户冻结/解冻使用管理员在后台系统对用户的惩罚措施.对于发布不当言论或者违法违规内容的用户,可以暂时.永久禁止其登录,评论,发布动态等行为. 管理员在管理系统中对某一个用户冻结 ...

  5. echarts 玫瑰图比例太小显示优化

    echarts 玫瑰图比例太小显示优化 1.效果图 2.实现思路 3.代码 选择了 echarts 做大数据统计,但是由于统计数据的差异太大,数据特别小的时候,使用玫瑰图展示会造成看不清楚扇区的问题, ...

  6. 分享一个统计代码块耗时的小工具

    相信很多人都遇到这样一个问题? 加班加点,上线了一个功能,但接口的响应时间有点长? 如何定位排查? 普通研发人员一般会采用下面方式,在很多位置点埋入开始时间戳.结束时间戳,相减便得到耗时.代码如下: ...

  7. R语言统计代码运行耗时实战:计算代码运行时间、使用proc.time函数计算代码运行时间

    R语言统计代码运行耗时实战:计算代码运行时间.使用proc.time函数计算代码运行时间 目录

  8. 织梦后台图片集图片内容框太小解决办法

    织梦后台图片集图片内容框太小解决办法 网站根目录找到 dede/templets/album_edit.htm.dede/templets/album_add.htm 文件 图集内容 将标记的800改 ...

  9. C++统计代码执行耗时

    C++统计代码执行耗时 文章目录 C++统计代码执行耗时 1. 需求 2. 代码结构 3. CMakeLists文件 4. 核心代码 5. 使用测试 1. 需求 统计项目中代码执行耗时 2. 代码结构 ...

最新文章

  1. 倒梁法matlab,在编制措施项目清单时,关于钢筋混凝土模版及支架费项目,应在清单中列明(    )...
  2. python字符串转归类_pandas剪切:如何将分类标签转换为字符串(否则无法导出到Excel)?
  3. C++ 类和对象(一):类的概念、类的访问控制和封装、类对象模型、this指针
  4. NYOJ--927--dfs--The partial sum problem
  5. linux下usb调试工具,LINUX USB调试
  6. NM的完整形式是什么?
  7. Pandas 数据处理三板斧,你会几板?
  8. 一句话可以改变人的一生
  9. python中的对象有哪些_python中的对象
  10. 【日常科普】浏览器网页视频自定义倍速播放(无需任何插件)
  11. c语言小车路径规划算法,基于路径规划的室内小车定位系统
  12. 网站被国外IP攻击 禁止海外IP访问网站的解决办法
  13. Ubuntu 16.04 一系列软件安装命令,包括QQ、搜狗、Chrome、vlc、网易云音乐安装方法
  14. 微服务之服务网关Gateway
  15. 微信小程序学习总结(旋转音乐盒)
  16. ubuntu 11.10使用fcitx双拼输入法
  17. LeanCloud 一至二月变化
  18. JVM:JVM常见参数配置
  19. 红帽 linux 安装gns3,在Arch Linux和Manjaro系统上安装GNS3的方法
  20. 技校计算机英语,技校计算机英语的一体化教学论文

热门文章

  1. Unity3d AR 增强现实技术列表(2016年3月31日更新)
  2. centos+gitlab+mysql_centos7安装配置gitlab(使用外部nginx)
  3. WPF——自动关闭无模式窗口
  4. 使用TensorFlow.js从网络摄像头进行实时AI情感检测
  5. C#坏习惯:通过不好的例子学习如何制作好的代码——第2部分
  6. python快速排序最简单写法_面试官:来,这位精神小伙,简简单单写个快速排序吧...
  7. python登录验证程序_python – 测试Flask登录和身份验证?
  8. 逐步回归 图像降维matlab,使用matlab对训练样本图像降维,并对测试图像使用变换矩阵降维并重构...
  9. python logging模块详解_python logging模块使用总结
  10. scss 变量_SCSS和Sass使用这种样式语言所需的所有CSS和SCSS代码