在 Spring 中动态管理定时任务,通过简单的一句自动注入 ThreadPoolTaskScheduler 对象的代码,即可轻松实现,参见 Spring动态管理定时任务——ThreadPoolTaskScheduler 。

一、问题抛出

但如果没有查看 ThreadPoolTaskScheduler 的源码,则要特别注意 ThreadPoolTaskScheduler 中,初始化 poolSize=1。源码如下:

@SuppressWarnings("serial")
public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport
		implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, TaskScheduler {
    private volatile int poolSize = 1;
    ....
}

所以将会导致多个任务串行方式执行,而如果前面的某个任务一直没执行结束(比如不限制次数的重试机制),则会使得后面的任务一直没有机会执行。

二、问题复现

复现日志1(正常情况):

ThreadPoolExecutor 没有shutdown会有什么问题 threadpooltaskscheduler_Spring定时任务

【结论一】:可以看到4个(corn=0 0 8 * * ?)的定时任务串行执行。

上面的日志里没打印线程名(log4j中用%t代表),如果打印出来,则将是同一线程。

jconsole查看线程名称:

ThreadPoolExecutor 没有shutdown会有什么问题 threadpooltaskscheduler_TaskScheduler_02

 也可验证 ThreadPoolTaskScheduler 默认 poolSize 只有1,所以上面的4个任务是串行执行。

复现日志2(异常情况):

ThreadPoolExecutor 没有shutdown会有什么问题 threadpooltaskscheduler_MBean_03

 【结论二】:任务7执行成功,紧接着轮到任务9,而我的业务要求每个任务失败必须一直重试,导致后面的任务没有机会执行。

三、解决方案

3.1 设置poolSize固定值

设置 poolSize为固定值5,如下:

/**
 * 数据同步定时任务调度器
 */
@Bean(autowire = Autowire.BY_NAME, name = "sync")
public ThreadPoolTaskScheduler threadPoolTaskScheduler4Sync() {
    ThreadPoolTaskScheduler syncScheduler = new ThreadPoolTaskScheduler();
    syncScheduler.setPoolSize(5);
    syncScheduler.setThreadGroupName("syncTg");
    syncScheduler.setThreadNamePrefix("syncThread-");
    return syncScheduler;
}
@Resource(name = "sync")
private ThreadPoolTaskScheduler threadPoolTaskScheduler;

注意,这里设置以后,Spring 帮我们通过 IOC 实例化好了一个 ThreadPoolTaskScheduler 对象,poolSize 属性为5,但是通过jconsole发现并没有创建出 5 个工作线程,通过调试发现,其内置的executor线程池 poolSize 为 0!

ThreadPoolExecutor 没有shutdown会有什么问题 threadpooltaskscheduler_Spring定时任务_04

我们开启多于 5 个任务来验证一下,jconsole 再次查看线程名称和个数为固定值 5:

ThreadPoolExecutor 没有shutdown会有什么问题 threadpooltaskscheduler_JMX_05

3.2 动态设置poolSize值

然而,实际场景中往往不能固定设置死 poolSize 的值。特别是对任务调度系统来说,当需要同时运行一些各自独立的毫无关联的任务时,就会受到固定值的限制,造成的结果就是,只有池子里的任务有执行结束后,池子之外的任务才有机会被加入执行。更糟的情况是,当池子里的任务都在因为异常或业务要求(比如出错无限重试)而导致池子永远无法得到释放,将导致固定值之外的任务永远不会被执行!

所以,我们要么初始化 poolSize 为一个相对大的数值,要么我们必须动态去改变 ThreadPoolTaskScheduler 的 poolSize 大小。显然,后者更合理且灵活一些,翻看 ThreadPoolTaskScheduler.setPoolSize(int)

>>>>  “This setting can be modified at runtime, for example through JMX.

ThreadPoolExecutor 没有shutdown会有什么问题 threadpooltaskscheduler_TaskScheduler_06

从而得知允许我们在运行时动态调整 poolSize 大小。

3.3.1 业务代码调整

具体做法是,在启动停止任务的 Controller 方法中,通过比较当前 poolSize 大小和期望值来调整。

3.3.2 Restful 接口调整

为 poolSize 大小的管理创建和实现一些 restful 接口,当需要调整时,通过接口调用的方式完成。

3.3.3 JMX 调整

主要步骤如下:

  1. 创建调整 poolSize 的接口 ThreadPoolTaskSchedulerMBean
  2. 创建一个类 ThreadPoolTaskSchedulerWrapper,实现step1中的接口,且设置成员变量为 ThreadPoolTaskScheduler 对象
  3. 注册 ThreadPoolTaskSchedulerWrapper 对象到 MBean 服务器

下面给出一个 JMX 操作或管理 MBean 的例子:

(1)创建操作接口,

public interface UserMBean {
    String getName();
    void SetName(String name);
    String getPasswd();
    void SetPasswd(String pwd);
    int add(int x, int y);
}

(2)实现接口,并添加一些成员变量,

public class User implements UserMBean {
    private String name;
    private String passwd;

    @Override
    public String getName() {
        return name;
    }
    @Override
    public void SetName(String name) {
        this.name = name;
    }
    @Override
    public String getPasswd() {
        return passwd;
    }
    @Override
    public void SetPasswd(String pwd) {
        this.passwd = pwd;
    }
    @Override
    public int add(int x, int y) {
        return x + y;
    }
}

(3)注册MBean,

import java.lang.management.ManagementFactory;

import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;

public class TestMBean {
    public static void main(String[] args) throws MalformedObjectNameException, InstanceAlreadyExistsException,
            MBeanRegistrationException, NotCompliantMBeanException, InterruptedException {
        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
        ObjectName objName = new ObjectName("jmx:type=User");
        User bean = new User();
        server.registerMBean(bean, objName);
        System.out.println("JMX started..");
        String oldName = null;
        String oldPwd = null;
        while (true) {
            if (oldName != bean.getName() || oldPwd != bean.getPasswd()) {
                System.out.println(bean.getName() + bean.getPasswd());
                oldName = bean.getName();
                oldPwd = bean.getPasswd();
            }
            Thread.sleep(1000L);
        }
    }
}

最后启动程序后,打开 jconsole, 再打开 MBean 标签页,找到注册的 MBean 对象User,执行操作调用即可。

ThreadPoolExecutor 没有shutdown会有什么问题 threadpooltaskscheduler_Spring定时任务_07

以上。