线程优化,@Schedule注解单线程,多个定时任务执行时造成阻塞不能按时执行

问题背景:
新增定时任务不能按时执行

原定时任务的实现方式:
通过注解@Scheduled(cron="XXXX")+启动类@EnableScheduling的方式

@Component
@Slf4j
public class scheduleTask {
    @Scheduled(cron = "0/1 * * * * ?")
    public void fiveSecondsTask2() {
        Date currentDate = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        String nowTime = format.format(currentDate);
        log.error("注解方式定时任务二: " + nowTime + " " + Thread.currentThread().getName());
    }

    @Scheduled(cron = "0/5 * * * * ?")
    public void fiveSecondsTask() throws InterruptedException {
        Date currentDate = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        String nowTime = format.format(currentDate);
        log.error("注解方式定时任务111: " + nowTime + " " + Thread.currentThread().getName());
        while (true) {
            // 模拟耗时任务
            log.error("定时任务111模拟耗时任务");
            Thread.sleep(1000 * 5);
            break;
        }
    }
}

--------------------此处有补充。。。-------

debug方式查看定时任务的执行步骤发现如下错误:(该异常被吞掉) 被吞掉的说法不严谨,可能是被上层catch处理。

public void com.example.myTest.crontask.scheduleTask.fiveSecondsTask1() throws java.lang.InterruptedException

解释:执行线程被中止异常

当某个线程处于长时间的等待、休眠或其他暂停状态,而此时其他的线程通过Thread的interrupt方法终止该线程时抛出该异常。

Java处理InterruptedException_Xiaowo-CSDN博客_java.lang.interruptedexception

--------------------补充结束。。。----------

造成定时任务不能按时执行的原因:

@Scheduled注解定时执行任务的时候是在一个单线程中,如果有多个任务,其中一个任务执行时间过长,则有可能会导致其他后续任务被阻塞直到该任务执行完成。也就是会造成一些任务无法定时执行的错觉。

源码:@Scheduled注解在注册时给定时任务生成了一个单线程的线程池(newSingleThreadScheduledExecutor())

Scheduled-->ScheduledAnnotationBeanPostProcessor-->ScheduledTaskRegistrar-->scheduleTasks方法

 protected void scheduleTasks() {
        if (this.taskScheduler == null) {
            this.localExecutor = Executors.newSingleThreadScheduledExecutor();
            this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
        }

解决办法一:

扩大原定时任务线程池中的核心线程数

修改定时任务默认使用线程池中的线程数为50

@Configuration
public class scheduleConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(50));
    }
}

这个方法,在程序启动后,会逐步启动50个线程,放在线程池中。每个定时任务会占用1个线程。但是相同的定时任务,执行的时候,还是在同一个线程中。

例如,程序启动,每个定时任务占用一个线程。任务1开始执行,任务2也开始执行。如果任务1卡死了,那么下个周期,任务1还是处于卡死状态,任务2可以正常执行。也就是说,任务1某一次卡死了,不会影响其他线程,但是他自己本身这个定时任务会一直等待上一次任务执行完成。

解决方法二:(√)

把Scheduled配置成成多线程执行

新增自定义线程池,配置线程池,启动类添加注解@EnableAsync

定时任务自动匹配线程池:@Async(配置文件中Bean的别名或线程池配置方法名)

新增配置文件: 

@Component
public class asyncScheduledTaskConfig {

    @Bean("myAsync")
    public Executor myAsync() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 最大线程数
        executor.setMaxPoolSize(100);
        // 核心线程数
        executor.setCorePoolSize(100);
        // 任务队列的大小
        executor.setQueueCapacity(100);
        // 线程前缀名
        executor.setThreadNamePrefix("New-Thread-");
        // 线程存活时间
        executor.setKeepAliveSeconds(60);
        // 拒绝处理策略:直接抛出异常
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy() {
        });
        // 线程初始化
        executor.initialize();
        return executor;
    }
}

定时任务类中自动匹配Bean:

@Scheduled(cron = "0/1 * * * * ?")
    @Async("myAsync")
    public void fiveSecondsAsyncTask1() {
        Date currentDate = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        String nowTime = format.format(currentDate);
        log.error("注解方式定时任务二: " + nowTime + " " + Thread.currentThread().getName());
    }

    @Scheduled(cron = "0/5 * * * * ?")
    @Async("myAsync")
    public void fiveSecondsAsyncTask2() throws InterruptedException {
        Date currentDate = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        String nowTime = format.format(currentDate);
        log.error("注解方式定时任务111: " + nowTime + " " + Thread.currentThread().getName());
        while (true) {
            // 模拟耗时任务
            log.error("定时任务111模拟耗时任务");
            Thread.sleep(1000 * 5);
            break;
        }
    }

启动类使用@EnableAsync注解 

@EnableAsync
@EnableScheduling
@ComponentScan
@SpringBootApplication
public class MyTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyTestApplication.class, args);
        System.out.println("----------------------start -------------------------");
    }

}

这种方法,每次定时任务启动的时候,都会创建一个单独的线程来处理。也就是说同一个定时任务也会启动多个线程处理。

例如:任务1和任务2一起处理,但是线程1卡死了,任务2是可以正常执行的。且下个周期,任务1还是会正常执行,不会因为上一次卡死了,影响任务1。

但是任务1中的卡死线程越来越多,会导致50个线程池占满,还是会影响到定时任务。

影响时的解决方法--重启。。。

/**

 * 拒绝处理策略

 * CallerRunsPolicy():交由调用方线程运行,比如 main 线程。

 * AbortPolicy():直接抛出异常。

 * DiscardPolicy():直接丢弃。

 * DiscardOldestPolicy():丢弃队列中最老的任务。

 */

 解决方法三:

定时任务方法内部异步执行

同一个类中的定时任务共用一个线程池。

ThreadPoolTaskExecutor线程池的使用:

Spring线程池ThreadPoolTaskExecutor的使用 - 上善若泪 - 博客园

参数解释:

①corePoolSize:核心线程数(同时处理的能力)

②queueCapacity:队列容量

③maxPoolSize:最大线程数;决定了当corePoolSize 以及queueCapacity 满了以后,会在线程中额外创建线程的

假如corePoolSize和queueCapacity均为5,而maxPoolSize等于12.那么当线程大于12的时候(也就是corePoolSize,queueCapacity都满且额外创建了7个线程,依旧有线程过来),那么大于12的线程会走拒绝策略,也就是maxPoolSize=corePoolSize+x(个线程(非队列))

④keepAliveSeconds:当任务处理完以后,线程池中的线程数还大于corePoolSize,那么多余的

线程会被回收。也就是超时回收时间

1. 处理流程

当一个任务被提交到线程池时,首先查看线程池的核心线程是否都在执行任务,否就选择一条线程执行任务,是就执行第二步。

查看核心线程池是否已满,不满就创建一条线程执行任务,否则执行第三步。

查看任务队列是否已满,不满就将任务存储在任务队列中,否则执行第四步。

查看线程池是否已满,不满就创建一条线程执行任务,否则就按照策略处理无法执行的任务。

在ThreadPoolExecutor中表现为:

如果当前运行的线程数小于corePoolSize,那么就创建线程来执行任务(执行时需要获取全局锁)。

如果运行的线程大于或等于corePoolSize,那么就把task加入BlockQueue。

如果创建的线程数量大于BlockQueue的最大容量,那么创建新线程来执行该任务。

如果创建线程导致当前运行的线程数超过maximumPoolSize,就根据饱和策略来拒绝该任务。

2. 关闭线程池

调用shutdown或者shutdownNow,两者都不会接受新的任务,而且通过调用要停止线程的interrupt方法来中断线程,有可能线程永远不会被中断,不同之处在于shutdownNow会首先将线程池的状态设置为STOP,然后尝试停止所有线程(有可能导致部分任务没有执行完)然后返回未执行任务的列表。而shutdown则只是将线程池的状态设置为shutdown,然后中断所有没有执行任务的线程,并将剩余的任务执行完。

3.配置线程个数

如果是CPU密集型任务,那么线程池的线程个数应该尽量少一些,一般为CPU的个数+1条线程。

如果是IO密集型任务,那么线程池的线程可以放的很大,如2*CPU的个数。

对于混合型任务,如果可以拆分的话,通过拆分成CPU密集型和IO密集型两种来提高执行效率;如果不能拆分的的话就可以根据实际情况来调整线程池中线程的个数。

4.监控线程池状态

常用状态:

taskCount:线程需要执行的任务个数。

completedTaskCount:线程池在运行过程中已完成的任务数。

largestPoolSize:线程池曾经创建过的最大线程数量。

getPoolSize获取当前线程池的线程数量。

getActiveCount:获取活动的线程的数量

通过继承线程池,重写beforeExecute,afterExecute和terminated方法来在线程执行任务前,线程执行任务结束,和线程终结前获取线程的运行情况,根据具体情况调整线程池的线程数量。

  • 5
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值