详解Java ScheduledThreadPoolExecutor的踩坑与解决方法

目录
  • 概述
  • 还原"大坑"
  • 解决方案
    • 更推荐的做法
  • 原理探究
  • 总结

概述

最近项目上反馈某个重要的定时任务突然不执行了,很头疼,开发环境和测试环境都没有出现过这个问题。定时任务采用的是ScheduledThreadPoolExecutor,后来一看代码发现踩了一个大坑....

还原"大坑"

这个坑就是如果ScheduledThreadPoolExecutor中执行的任务出错抛出异常后,不仅不会打印异常堆栈信息,同时还会取消后面的调度, 直接看例子。

@Test
public void testException() throws InterruptedException {
    // 创建1个线程的调度任务线程池
    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
    // 创建一个任务
    Runnable runnable = new Runnable() {

        volatile int num = 0;

        @Override
        public void run() {
            num ++;
            // 模拟执行报错
            if(num > 5) {
                throw new RuntimeException("执行错误");
            }
            log.info("exec num: [{}].....", num);
        }
    };

    // 每隔1秒钟执行一次任务
    scheduledExecutorService.scheduleAtFixedRate(runnable, 0, 1, TimeUnit.SECONDS);
    Thread.sleep(10000);
}

运行结果:

  • 只执行了5次后,就不打印,不执行了,因为报错了
  • 任务报错,也没有打印一次堆栈,更导致调度任务取消,后果十分严重。

解决方案

解决方法也非常简单,只要通过try catch捕获异常即可。

运行结果:

看到不仅打印了异常堆栈,而且也会进行周期性的调度。

更推荐的做法

更好的建议可以在自己的项目中封装一个包装类,要求所有的调度都提交通过我们统一的包装类, 如下代码:

@Slf4j
public class RunnableWrapper implements Runnable {
    // 实际要执行的线程任务
    private Runnable task;
    // 线程任务被创建出来的时间
    private long createTime;
    // 线程任务被线程池运行的开始时间
    private long startTime;
    // 线程任务被线程池运行的结束时间
    private long endTime;
    // 线程信息
    private String taskInfo;

    private boolean showWaitLog;

    /**
     * 执行间隔时间多久,打印日志
     */
    private long durMs = 1000L;

    // 当这个任务被创建出来的时候,就会设置他的创建时间
    // 但是接下来有可能这个任务提交到线程池后,会进入线程池的队列排队
    public RunnableWrapper(Runnable task, String taskInfo) {
        this.task = task;
        this.taskInfo = taskInfo;
        this.createTime = System.currentTimeMillis();
    }

    public void setShowWaitLog(boolean showWaitLog) {
        this.showWaitLog = showWaitLog;
    }

    public void setDurMs(long durMs) {
        this.durMs = durMs;
    }

    // 当任务在线程池排队的时候,这个run方法是不会被运行的
    // 但是当任务结束了排队,得到线程池运行机会的时候,这个方法会被调用
    // 此时就可以设置线程任务的开始运行时间
    @Override
    public void run() {
        this.startTime = System.currentTimeMillis();

        // 此处可以通过调用监控系统的API,实现监控指标上报
        // 用线程任务的startTime-createTime,其实就是任务排队时间
        // 这边打印日志输出,也可以输出到监控系统中
        if(showWaitLog) {
            log.info("任务信息: [{}], 任务排队时间: [{}]ms", taskInfo, startTime - createTime);
        }

        // 接着可以调用包装的实际任务的run方法
        try {
            task.run();
        } catch (Exception e) {
            log.error("run task error", e);
            throw e;
        }

        // 任务运行完毕以后,会设置任务运行结束的时间
        this.endTime = System.currentTimeMillis();

        // 此处可以通过调用监控系统的API,实现监控指标上报
        // 用线程任务的endTime - startTime,其实就是任务运行时间
        // 这边打印任务执行时间,也可以输出到监控系统中
        if(endTime - startTime > durMs) {
            log.info("任务信息: [{}], 任务执行时间: [{}]ms", taskInfo, endTime - startTime);
        }

    }
}

使用:

我们还可以在包装类里面封装各种监控行为,如本例打印日志执行时间等。

原理探究

那大家有没有想过为什么任务出错会导致异常无法打印,甚至调度都取消了呢?让我们从源码出发,一探究竟。

1.下面是调度任务的入口方法。

// ScheduledThreadPoolExecutor#scheduleAtFixedRate
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0)
        throw new IllegalArgumentException();
    // 将执行任务和参数包装成ScheduledFutureTask对象
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      unit.toNanos(period));
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    // 延迟执行
    delayedExecute(t);
    return t;
}

这个方法主要做了两个事情:

  • 将执行任务和参数包装成ScheduledFutureTask对象
  • 调用delayedExecute方法延迟执行任务

2.延迟或周期性任务的主要执行方法, 主要是将任务丢到队列中,后续由工作线程获取执行。

// ScheduledThreadPoolExecutor#delayedExecute
private void delayedExecute(RunnableScheduledFuture<?> task) {
        if (isShutdown())
            reject(task);
        else {
            // 将任务丢到阻塞队列中
            super.getQueue().add(task);
            if (isShutdown() &&
                !canRunInCurrentRunState(task.isPeriodic()) &&
                remove(task))
                task.cancel(false);
            else
                // 开启工作线程,去执行任务,或者从队列中获取任务执行
                ensurePrestart();
        }
    }

3.现在任务已经在队列中了,我们看下任务执行的内容是什么,还记得前面的包装对象ScheduledFutureTask类,它的实现类是ScheduledFutureTask,继承了Runnable类。

// ScheduledFutureTask#run方法
public void run() {
    // 是不是周期性任务
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    // 不是周期性任务的话, 直接调用一次下面的run
    else if (!periodic)
        ScheduledFutureTask.super.run();
    // 如果是周期性任务,则调用runAndReset方法,如果返回true,继续执行
    else if (ScheduledFutureTask.super.runAndReset()) {
        // 设置下次调度时间
        setNextRunTime();
        // 重新执行调度任务
        reExecutePeriodic(outerTask);
    }
}

这里的关键就是看ScheduledFutureTask.super.runAndReset()方法是否返回true,如果是true的话继续调度。

4.runAndReset方法也很简单,关键就是看报异常如何处理。

// FutureTask#runAndReset
protected boolean runAndReset() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return false;
    // 是否继续下次调度,默认false
    boolean ran = false;
    int s = state;
    try {
        Callable<V> c = callable;
        if (c != null && s == NEW) {
            try {
                // 执行任务
                c.call();
                // 执行成功的话,设置为true
                ran = true;

                // 异常处理,关键点
            } catch (Throwable ex) {
                // 不会修改ran的值,最终是false,同时也不打印异常堆栈
                setException(ex);
            }
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
    // 返回结果
    return ran && s == NEW;
}
  • 关键点ran变量,最终返回是不是下次继续调度执行
  • 如果抛出异常的话,可以看到不会修改ran为true。

总结

Java的ScheduledThreadPoolExecutor定时任务线程池所调度的任务中如果抛出了异常,并且异常没有捕获直接抛到框架中,会导致ScheduledThreadPoolExecutor定时任务不调度了。这个结论希望大家一定要记住,不然非常坑,关键是有时候测试环境、开发环境还无法复现,有一定的随机性,真的到了生产就完蛋了。

到此这篇关于详解Java ScheduledThreadPoolExecutor的踩坑与解决方法的文章就介绍到这了,更多相关Java ScheduledThreadPoolExecutor内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java 定时器线程池(ScheduledThreadPoolExecutor)的实现

    前言 定时器线程池提供了定时执行任务的能力,即可以延迟执行,可以周期性执行.但定时器线程池也还是线程池,最底层实现还是ThreadPoolExecutor,可以参考我的另外一篇文章多线程–精通ThreadPoolExecutor. 特点说明 1.构造函数 public ScheduledThreadPoolExecutor(int corePoolSize) { // 对于其他几个参数在ThreadPoolExecutor中都已经详细分析过了,所以这里,将不再展开 // 这里我们可以看到调用基类

  • java高并发ScheduledThreadPoolExecutor与Timer区别

    目录 正文 二者的区别 线程角度 系统时间敏感度 是否捕获异常 任务是否具备优先级 是否支持对任务排序 能否获取返回的结果 二者简单的示例 Timer类简单示例 ScheduledThreadPoolExecutor类简单示例 正文 JDK 1.5开始提供ScheduledThreadPoolExecutor类,ScheduledThreadPoolExecutor类继承ThreadPoolExecutor类重用线程池实现了任务的周期性调度功能.在JDK 1.5之前,实现任务的周期性调度主要使用

  • Java调度线程池ScheduledThreadPoolExecutor不执行问题分析

    目录 前言 ScheduledThreadPoolExecutor不调度分析 示例程序 源码分析 总结 前言 最近在调试一个监控应用指标的时候发现定时器在服务启动执行一次之后就不执行了,这里用的定时器是Java的调度线程池ScheduledThreadPoolExecutor,后来经过排查发现ScheduledThreadPoolExecutor线程池处理任务如果抛出异常,会导致线程池不调度:下面就通过一个例子简单分析下为什么异常会导致ScheduledThreadPoolExecutor不执行

  • Java自带定时任务ScheduledThreadPoolExecutor实现定时器和延时加载功能

    java.util.concurrent.ScheduledThreadPoolExecutor 是JDK1 .6之后自带的包,功能强大,能实现定时器和延时加载的功能 各类功能和处理方面优于Timer 1.定时器: ScheduledThreadPoolExecutor  有个scheduleAtFixedRate(command, initialDelay, period, unit) ;方法 command: 执行的线程(可自己New一个) initialDelay:初始化执行的延时时间 p

  • ScheduledThreadPoolExecutor巨坑解决

    目录 概述 坑是啥? 怎么坑的? 总结 概述 最近在做一些优化的时候用到了ScheduledThreadPoolExecutor. 虽然知道这个玩意,但是也很久没用,本着再了解了解的心态,到网上搜索了一下,结果就发现网上有些博客在说ScheduledThreadPoolExecutor有巨坑!!! 瞬间,我的兴趣就被激起来了,马上进去学习了一波- 不看不知道,看完后马上把我的代码坑给填上了- 下面就当记录一下吧,顺便也带大家了解了解,大家看完后也赶紧看看自己公司的项目代码有没有这种漏洞,有的话赶

  • 详解Java分布式缓存系统中必须解决的四大问题

    目录 缓存穿透 缓存击穿 缓存雪崩 缓存一致性 分布式缓存系统是三高架构中不可或缺的部分,极大地提高了整个项目的并发量.响应速度,但它也带来了新的需要解决的问题,分别是: 缓存穿透.缓存击穿.缓存雪崩和缓存一致性问题. 缓存穿透 第一个比较大的问题就是缓存穿透.这个概念比较好理解,和命中率有关.如果命中率很低,那么压力就会集中在数据库持久层. 假如能找到相关数据,我们就可以把它缓存起来.但问题是,本次请求,在缓存和持久层都没有命中,这种情况就叫缓存的穿透. 举个例子,如上图,在一个登录系统中,有

  • 详解Java中List的正确的删除方法

    目录 简介 实例 正确方法 法1:for的下标倒序遍历 法2: list.stream().filter().collect() 法3: iterator迭代器 错误方法 法1:for(xxx : yyy)遍历 法2:for的下标正序遍历 原因分析 简介 本文介绍Java的List的正确的删除方法. 实例 需求:有如下初始数据,将list中的所有数据为"b"的元素删除掉.即:填充removeB()方法 package com.example.a; import java.util.Ar

  • 详解Java中异步转同步的六种方法

    目录 一.问题 应用场景 二.分析 三.实现方法 1.轮询与休眠重试机制 2.wait/notify 3.Lock Condition 4.CountDownLatch 5.CyclicBarrier 6.LockSupport 一.问题 应用场景 应用中通过框架发送异步命令时,不能立刻返回命令的执行结果,而是异步返回命令的执行结果. 那么,问题来了,针对应用中这种异步调用,能不能像同步调用一样立刻获取到命令的执行结果,如何实现异步转同步? 二.分析 首先,解释下同步和异步 同步,就是发出一个调

  • Mybatis详解在注解sql时报错的解决方法

    目录 错误: 文件结构 BookMapper.java BookMapperSQL .java Mybatis的配置文件 分析: 错误: 在做Mybatis用注解方式来注入sql的练习时,报了这样子的错误. 遇到错误很正常,然后我又从学了一遍今天刚刚学的内容,温故而知新嘛. 错误问题如下: 文件结构 BookMapper.java public interface BookMapper { @SelectProvider(type = BookMapperSQL.class,method = "

  • 基于IOS端微信分享失效的踩坑及解决方法

    最近的一个公众号是基于vue的spa应用,在接入微信分享和微信语音的时候出现了:在Android上一切正常,但是在ios端调用wx.config的时候总是失败,去翻了官方文档也并没有找到解决方案,最后在测试中发现是因为初始化的时候传入的URL的问题.具体过程如下: 微信config接口配置,官方文档如下: 所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支

  • 详解selenium + chromedriver 被反爬的解决方法

    问题背景:这个问题是在爬取某夕夕商城遇到的问题,原本的方案是用selenium + chromedriver + mitmproxy开心的刷,但是几天之后,发现刷不出来了,会直接跳转到登陆界面(很明显,是遭遇反爬了) 讲实话,这还是第一次用硒被反爬的,于是进行大规模的测试对比. 同台机器,用铬浏览器正常访问是不用跳转到登陆界面的,所以不是IP的问题.再用提琴手抓包对比了一下两个请求头,请求头都是一样的,所以忽略标头的反爬. 最后通过分析,可能是硒被检测出来了.于是就去查资料.大概的查到是和web

  • 详解Java向服务端发送文件的方法

    本文实例为大家分享了Java向服务端发送文件的方法,供大家参考,具体内容如下 /* *给服务端发送文件,主要是IO流. */ import java.io.*; import java.net.*; class send2 { public static void main(String[] args) throws Exception { Socket s = new Socket("192.168.33.1",10005);//建立服务 BufferedReader bufr =

  • python下setuptools的安装详解及No module named setuptools的解决方法

    前言 python下的setuptools带有一个easy_install的工具,在安装python的每三方模块.工具时很有用,也很方便. 安装setuptools前先安装pip,请参考:linux下pip的安装步骤及使用详解 1. 下载: 在它的官网可以下载到安装包: https://pypi.python.org/pypi/setuptools 页面最下面的是它的安装链接,如: $wget --no-check-certificate https://pypi.python.org/pack

  • 详解Spring中实现接口动态的解决方法

    前言 本文主要给大家介绍的是关于Spring实现接口动态的相关内容,分享出来供大家参考学习,下面话不多说,来一起看看详细的介绍吧. 关于这个问题是因为领导最近跟我提了一个需求,是有关于实现类Mybatis的@Select.@Insert注解的功能.其是基于interface层面,不存在任何的接口实现类.因而在实现的过程中,首先要解决的是如何动态实现接口的实例化.其次是如何将使接口根据注解实现相应的功能. 声明 解决方案是基于Mybatis源码,进行二次开发实现. 解决方法 我们先来看看Mybat

  • 详解升级react-router 4 踩坑指南

    一.前言 上午把近日用React做的一个新闻项目所依赖的包升级到了最新的版本,其中从react-router(2.8.1)升级到react-router(4.1.2)中出现了很多问题, 故总结一下在升级过程中遇到的问题. 二.react-router,V4版本修改内容 1. 所有组件更改为从react-router-dom导入 之前的所有路由组件均是从react-router中导入,在我之前的项目中,导入相关组件如下: //v2 import {Router,Route,hashHistory}

随机推荐