springboot异步线程

前言

最近项目中出现了一个问题,发现自己的定时器任务在线上没有执行,但是在线下测试时却能执行,最后谷歌到了这篇文章SpringBoot踩坑日记-定时任务不定时了?;

本篇文章主要以自己在项目中遇到的问题为背景,并不涉及源码;

Scheduled 定时任务

Scheduled注解的具体使用方法自行百度或谷歌,这里只是使用其中的一种方式;

验证Scheduled为单线程执行

  1. 测试代码

@Component
public class TestScheduling {

    private static final Logger log= LoggerFactory.getLogger(TestScheduling.class);
    
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testOne() {
        //打印线程名字
        log.info("ThreadName:====one====" + Thread.currentThread().getName());
    }
    
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testTwo(){
        //打印线程名字
        log.info("ThreadName:====two====" + Thread.currentThread().getName());
    }
}

执行结果:


2019-11-13 16:09:07.205  INFO 26976 --- [   scheduling-1] c.example.async.timetask.TestScheduling  : ThreadName:====one====scheduling-1
2019-11-13 16:09:07.205  INFO 26976 --- [   scheduling-1] c.example.async.timetask.TestScheduling  : ThreadName:====two====scheduling-1
2019-11-13 16:09:37.206  INFO 26976 --- [   scheduling-1] c.example.async.timetask.TestScheduling  : ThreadName:====one====scheduling-1
2019-11-13 16:09:37.206  INFO 26976 --- [   scheduling-1] c.example.async.timetask.TestScheduling  : ThreadName:====two====scheduling-1

弊端

如若有多个定时器时,当其中某个任务出现死循环或一直等待时,所有的定时任务将不会执行,这就是上面说的本地定时任务能执行,线上运行久了,定时任务就不会执行了;

解决方案

1. 异步注解@Async

在所执行的定时任务的方法(方法为public)上添加@Async注解,@Async注解的具体用法自行百度或谷歌,这里使用其中的一种方式。

  1. 代码

@Component
public class TestScheduling {

    private static final Logger log = LoggerFactory.getLogger(TestScheduling.class);
    
    @Async
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testOne() {
        //打印线程名字
        log.info("ThreadName:====one====" + Thread.currentThread().getName());
    }
    
    @Async
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testTwo() {
        //打印线程名字
        log.info("ThreadName:====two====" + Thread.currentThread().getName());
    }
}
  1. 结果

2019-11-13 16:59:16.394  INFO 36964 --- [cTaskExecutor-2] c.example.async.timetask.TestScheduling  : ThreadName:====one====SimpleAsyncTaskExecutor-2
2019-11-13 16:59:16.394  INFO 36964 --- [cTaskExecutor-1] c.example.async.timetask.TestScheduling  : ThreadName:====two====SimpleAsyncTaskExecutor-1
2019-11-13 16:59:46.391  INFO 36964 --- [cTaskExecutor-3] c.example.async.timetask.TestScheduling  : ThreadName:====two====SimpleAsyncTaskExecutor-3
2019-11-13 16:59:46.391  INFO 36964 --- [cTaskExecutor-4] c.example.async.timetask.TestScheduling  : ThreadName:====one====SimpleAsyncTaskExecutor-4

可以发现,线程虽然不是同一个线程了,但是定时任务每一次执行都是新创建的线程,用完之后就销毁,这样频繁的创建销毁线程是很费资源的;

怎么发现线程被销毁,在Windows系统上可以使用Java自带的jvisualvm.exe工具来查看应用的运行状况;

2. 使用异步注解+线程池

为什么这么说,百度谷歌了很多文章,都是创建了线程池,最后异步注解却没有用创建的线程池。

  1. 创建线程池
    创建线程池的方式很多种,这里使用其中的一种:

@Configuration
public class AsyncConfig {
    @Bean
    public AsyncTaskExecutor asyncTaskExecutor() {
    
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
        executor.setThreadNamePrefix("courses-schedule-");
        //最大线程数10:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(10);
        //核心线程数3:线程池创建时候初始化的线程数
        executor.setCorePoolSize(3);
        //缓冲队列0:用来缓冲执行任务的队列
        executor.setQueueCapacity(5);
        //允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(60);
        // 当线程池已满,且等待队列也满了的时候,直接抛弃当前线程(不会抛出异常)
//        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.initialize();
        return executor;
    }
}

这里的线程池名字为:asyncTaskExecutor,就是方法名;其中线程池里面的属性根据自己的情况而定;

这一步很多文章都写到了,而且创建线程池的方式也各种各样;

  1. 注解上使用线程池

@Component
public class TestScheduling {

    private static final Logger log = LoggerFactory.getLogger(TestScheduling.class);
    
    @Async("asyncTaskExecutor")
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testOne() {
        //打印线程名字
        log.info("ThreadName:====one====" + Thread.currentThread().getName());
    }
    
    @Async("asyncTaskExecutor")
    @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30)
    public void testTwo() {
        //打印线程名字
        log.info("ThreadName:====two====" + Thread.currentThread().getName());
    }
}

注意: 这里在@Async注解里面添加了线程名字为asyncTaskExecutor

  1. 执行结果

2019-11-13 17:22:52.021  INFO 5448 --- [rses-schedule-2] c.example.async.timetask.TestScheduling  : ThreadName:====one====courses-schedule-2
2019-11-13 17:22:52.021  INFO 5448 --- [rses-schedule-3] c.example.async.timetask.TestScheduling  : ThreadName:====two====courses-schedule-3
2019-11-13 17:23:22.021  INFO 5448 --- [rses-schedule-1] c.example.async.timetask.TestScheduling  : ThreadName:====two====courses-schedule-1
2019-11-13 17:23:22.021  INFO 5448 --- [rses-schedule-2] c.example.async.timetask.TestScheduling  : ThreadName:====one====courses-schedule-2
2019-11-13 17:23:52.021  INFO 5448 --- [rses-schedule-3] c.example.async.timetask.TestScheduling  : ThreadName:====two====courses-schedule-3
2019-11-13 17:23:52.021  INFO 5448 --- [rses-schedule-1] c.example.async.timetask.TestScheduling  : ThreadName:====one====courses-schedule-1
2019-11-13 17:24:22.022  INFO 5448 --- [rses-schedule-2] c.example.async.timetask.TestScheduling  : ThreadName:====two====courses-schedule-2
2019-11-13 17:24:22.022  INFO 5448 --- [rses-schedule-3] c.example.async.timetask.TestScheduling  : ThreadName:====one====courses-schedule-3

调用使用@Async注解的方法

这个主要还是自己基础太差的原因,在调用有@Async的方法的时候,注意调用该方法的方式,有些方式并不会使@Async注解产生作用。

比如以下情况:


@Component
public class AsyncTest {
    @Async
    public void testOne(){
        System.out.println("ThreadName:===one===="+Thread.currentThread().getName());
    }
}

@Service
public class AsyncServiceImpl implements AsyncService {
    @Autowired
    private AsyncTest asyncTest;
    @Override
    public void testAsync() {
        // 步骤一
        asyncTest.testOne();
        // 步骤二
        testOne();
    }
    @Async
    public void testOne() {
        System.out.println("ThreadName:===AsyncServiceImpl+testOne====" + Thread.currentThread().getName());
    }

}

这里注意步骤一和二,两个方法都是用了@Async注解的,但是步骤二就是方法调用,并不是异步调用;

总结

经验是不断积累的,需要学习的地方还有很多,继续努力;

参考:

上一篇:JVM - Java虚拟机规范官方文档


下一篇:Hexo+Github博客:新建菜单,并在该菜单内添加单篇/多篇文章