springboot定时任务,异步调用,自定义线程池和优雅关闭,Future接口主线程等子线程

1. @Scheduled创建定时任务

  • https://blog.didispace.com/spring-boot-learning-1x/

创建定时任务

@SpringBootApplication
@EnableScheduling
public class Application {

}
@Component
public class ScheduledTasks {

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    @Scheduled(fixedRate = 5000)
    public void reportCurrentTime() {
        System.out.println("现在时间:" + dateFormat.format(new Date()));
    }

}
现在时间:20:38:59
现在时间:20:39:04
现在时间:20:39:09
现在时间:20:39:14
现在时间:20:39:19

@Scheduled详解

@Scheduled(fixedRate = 5000) 注解来定义每过5秒执行的任务,

如下几种方式:

  • @Scheduled(fixedRate = 5000) :上一次开始执行时间点之后5秒再执行
  • @Scheduled(fixedDelay = 5000) :上一次执行完毕时间点之后5秒再执行
  • @Scheduled(initialDelay=1000, fixedRate=5000) :第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次
  • @Scheduled(cron="*/5 * * * * *") :通过cron表达式定义规则

2. @Async实现异步调用

什么是“异步调用”?

“异步调用”对应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;

异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。

异步调用

若这三个任务本身之间不存在依赖关系,可以并发执行的话,同步调用在执行效率方面就比较差

只需要通过使用@Async注解就能简单的将原来的同步函数变为异步函数

//统一加在方法上,也可以
@Component
public class Task {

    @Async
    public void doTaskOne() throws Exception {
        // 同上内容,省略
    }

    @Async
    public void doTaskTwo() throws Exception {
        // 同上内容,省略
    }

}

让@Async注解能够生效,还需要在Spring Boot的主程序中配置@EnableAsync

开始做任务一
开始做任务二
开始做任务三
完成任务二,耗时:1795毫秒
完成任务三,耗时:4556毫秒
完成任务一,耗时:6702毫秒

开始做任务二
开始做任务三
开始做任务一
完成任务二,耗时:220毫秒
完成任务一,耗时:5811毫秒
完成任务三,耗时:6714毫秒

配置

@SpringBootApplication
@EnableAsync//默认是开启的
public class Application {
}

主程序在异步调用之后,主程序并不会理会这三个函数是否执行完成了,由于没有其他需要执行的内容,所以程序就自动结束了,导致了不完整或是没有输出任务相关内容的情况。

注: @Async所修饰的函数不要定义为static类型,这样异步调用不会生效

基本的类

@Component
public class Task {

    public static Random random =new Random();

    public void doTaskOne() throws Exception {
        System.out.println("开始做任务一");
        
        //记录开始时间
        long start = System.currentTimeMillis();
        
        //睡眠随机10秒内的数
        Thread.sleep(random.nextInt(10000));
        
        //记录结束时间
        long end = System.currentTimeMillis();
        
        System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
    }

   //doTaskTwo()

   //doTaskThree()

}
开始做任务一
完成任务一,耗时:6894毫秒

开始做任务二
完成任务二,耗时:8335毫秒

开始做任务三
完成任务三,耗时:5835毫秒

异步回调

  • 接口返回:Future
  • 真实返回:new AsyncResult<>(“任务一完成”);

为了让doTaskOnedoTaskTwodoTaskThree能正常结束,假设我们需要统计一下三个任务并发执行共耗时多少,这就需要等到上述三个函数都完成调动之后记录时间,并计算结果。

如何判断上述三个异步调用是否已经执行完成呢?我们需要使用Future<T>来返回异步调用的结果,就像如下方式改造doTaskOne函数:

@Async
public Future<String> doTaskOne() throws Exception {
    System.out.println("开始做任务一");
    
    long start = System.currentTimeMillis();
    Thread.sleep(random.nextInt(10000));
    long end = System.currentTimeMillis();
    
    System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
    
    return new AsyncResult<>("任务一完成");
}

改造一下测试用例,让测试在等待完成三个异步调用之后来做一些其他事情。

long start = System.currentTimeMillis();

	Future<String> task1 = task.doTaskOne();
	Future<String> task2 = task.doTaskTwo();
	Future<String> task3 = task.doTaskThree();

	while(true) {
		if(task1.isDone() && task2.isDone() && task3.isDone()) {
			// 三个任务都调用完成,退出循环等待
			break;
		}
		Thread.sleep(1000);
	}

	long end = System.currentTimeMillis();

	System.out.println("任务全部完成,总耗时:" + (end - start) + "毫秒");

看看我们做了哪些改变:

  • 在测试用例一开始记录开始时间
  • 在调用三个异步函数的时候,返回Future<String>类型的结果对象
  • 在调用完三个异步函数之后,开启一个循环,根据返回的Future<String>对象来判断三个异步函数是否都结束了。若都结束,就结束循环;若没有都结束,就等1秒后再判断。
  • 跳出循环之后,根据结束时间 - 开始时间,计算出三个任务并发执行的总耗时。

执行一下上述的单元测试,可以看到如下结果:

开始做任务一
开始做任务二
开始做任务三
完成任务三,耗时:37毫秒
完成任务二,耗时:3661毫秒
完成任务一,耗时:7149毫秒
任务全部完成,总耗时:8025毫秒

3. @Async实现异步调用:自定义线程池

通过自定义线程池的方式来控制异步调用的并发。

定义线程池

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    //开启异步
    @EnableAsync
    //配置类
    @Configuration
    class TaskPoolConfig {
		//bean名称
        @Bean("taskExecutor")
        public Executor taskExecutor() {
            //ThreadPool Task Executor 线程池 任务 执行器
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            //核心线程数
            executor.setCorePoolSize(10);
            //最大线程数
            executor.setMaxPoolSize(20);
            //缓冲队列200
            executor.setQueueCapacity(200);
            //允许线程的空闲时间60秒
            executor.setKeepAliveSeconds(60);
            //线程池名的前缀
            executor.setThreadNamePrefix("taskExecutor-");
            
            //线程池对拒绝任务的处理策略
            executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            return executor;
        }
    }

}
capacity 
英 /kəˈpæsəti/  美 /kəˈpæsəti/  全球(英国)  
简明 牛津 新牛津  韦氏  柯林斯 例句  百科
n. 能力,才能;容积,容纳能力;职位,职责;功率,容积;生产量,生产能力
adj. 无虚席的,满场的

设置了以下这些参数:

  • 核心线程数10:线程池创建时候 初始化的线程数

  • 最大线程数20:线程池最大的线程数,

    • 只有在缓冲队列 满了之后 才会申请 超过核心线程数 的线程
  • 缓冲队列200:用来缓冲执行任务的队列

  • 允许线程的空闲时间60秒:

    • 当超过了核心线程出之外的线程 在空闲时间到达之后 会被销毁
  • 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池

  • 线程池对拒绝任务的处理策略:这里采用了CallerRunsPolicy策略,

    • 当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中 运行被拒绝的任务;
    • 如果执行程序已关闭,则会丢弃该任务
  • 核心10个。最大20。缓冲队列200。有毛病,最大应该设置特别大。

  • 即:超过10个后,还可在 缓冲200个,但不能超过最大线程。

使用线程池

只需要在@Async注解中指定线程池名即可,比如:

@Slf4j
@Component
public class Task {

    public static Random random = new Random();

    @Async("taskExecutor")
    public void doTaskOne() throws Exception {
        log.info("开始做任务一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务一,耗时:" + (end - start) + "毫秒");
    }
    
}

单元测试重要

//@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {

    @Autowired
    private Task task;

    //测试,就是主方法执行完毕,就销毁
    @Test
    public void test() throws Exception {

        task.doTaskOne();
        task.doTaskTwo();
        task.doTaskThree();

        //不和action一样(action的话,后台会都执行完),如果不做拦截。主线程执行完毕,就结束了。
        //直接拦截,卡死当前线程。里面加参数,如5000,只会卡死5秒。
        Thread.currentThread().join();
    }

}
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        //@RunWith(SpringJUnit4ClassRunner.class) 注释了更好。

thread.Join

  • 把指定的线程加入到当前线程,可以将两个交替执行的线程合并为 顺序执行的线程。

  • 比如在线程B 中调用了 线程A的 Join()方法,

    • 直到线程A执行完毕后,才会继续执行线程B。
  • t.join(); //调用join方法,等待线程t执行完毕

  • t.join(1000); //等待 t 线程,等待时间是1000毫秒。

4. ThreadPoolTaskScheduler线程池的优雅关闭

改造 task

改造之前的异步任务,让它依赖一个外部资源,比如:Redis

@Slf4j
@Component
public class Task {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Async("taskExecutor")
    public void doTaskOne() throws Exception {
        log.info("开始做任务一");
        long start = System.currentTimeMillis();
        log.info(stringRedisTemplate.randomKey());
        long end = System.currentTimeMillis();
        log.info("完成任务一,耗时:" + (end - start) + "毫秒");
    }
    
}

改造 test方法,模拟高并发

注意:这里省略了pom.xml中引入依赖和配置redis的步骤

模拟高并发情况下ShutDown的情况:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {

    @Autowired
    private Task task;

    @Test
    @SneakyThrows
    public void test() {

        for (int i = 0; i < 10000; i++) {
            task.doTaskOne();
            task.doTaskTwo();
            task.doTaskThree();

            if (i == 9999) {
                System.exit(0);
            }
        }
    }

}

说明:通过for循环往上面定义的线程池中提交任务,

由于是异步执行,在执行过程中,利用System.exit(0)来关闭程序,此时由于有任务在执行,就可以观察这些异步任务的销毁

  • 与Spring容器中其他资源的顺序是否安全。

第四步:运行上面的单元测试,我们将碰到下面的异常内容。

Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
	at redis.clients.util.Pool.getResource(Pool.java:53) ~[jedis-2.9.0.jar:na]
	at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226) ~[jedis-2.9.0.jar:na]
	at redis.clients.jedis.JedisPool.getResource(JedisPool.java:16) ~[jedis-2.9.0.jar:na]
	at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:194) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
	... 19 common frames omitted
Caused by: java.lang.InterruptedException: null

如何解决

原因分析

从异常信息JedisConnectionException: Could not get a resource from the pool来看,我们很容易的可以想到,

  • 在应用关闭的时候异步任务还在执行,由于Redis连接池先销毁了,
  • 导致异步任务中要访问Redis的操作就报了上面的错。
  • 所以,我们得出结论,上面的实现方式在应用关闭的时候是不优雅的,那么我们要怎么做呢?

解决方法

要解决上面的问题很简单,Spring的ThreadPoolTaskScheduler为我们提供了相关的配置,只需要加入如下设置即可:

@Bean("taskExecutor")
public Executor taskExecutor() {
    ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
    executor.setPoolSize(20);
    executor.setThreadNamePrefix("taskExecutor-");
    //等待 任务 完成后 在关闭
    executor.setWaitForTasksToCompleteOnShutdown(true);
    //等待 超过这个时间,就自动销毁
    executor.setAwaitTerminationSeconds(60);
    return executor;
}

线程池关闭的时候 等待所有任务都完成再继续销毁其他的Bean,这样这些异步任务的销毁就会先于Redis线程池的销毁。

同时,这里还设置了setAwaitTerminationSeconds(60)

  • 该方法用来设置线程池中任务的等待时间,
  • 如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
termination 
英 /ˌtɜːmɪˈneɪʃn/  美 /ˌtɜːrmɪˈneɪʃn/  全球(英国)  
简明 牛津 新牛津  韦氏  例句  百科
n. 终止妊娠,人工流产;结束,终止;<美>解聘,解雇;<美>暗杀;词尾(尤指屈折变化或派生词的词尾);<古>结局

terminate 
英 /ˈtɜːmɪneɪt/  美 /ˈtɜːrmɪneɪt/  全球(美国)  
简明 牛津 新牛津  韦氏  柯林斯 例句  百科
v. (使)结束,(使)终止;到达终点站;终止妊娠,人工流产;<美>解雇;<美>谋杀(某人);在……结尾,以……收尾
adj. 结束的

termina 
英  美  全球(罗马尼亚)  
简明 例句  百科
n. 接线柱;终端
网络释义
 完成
 目的
 
term 
英 /tɜːm/  美 /tɜːrm/  全球(美国)  
简明 牛津 新牛津  韦氏  柯林斯 例句  百科
n. (某人做某事或某事发生的)时期,期限,任期;学期,开庭期;到期,期满;术语,专有名词;措辞,说话的方式(terms);方面,角度(terms);(人们之间的)关系(terms);条件,条款(terms);付款条件,购买(出售)条件(terms);(结束战争,解决争端的)条件(terms);(数学运算中的)项;(逻)(三段论中的)项;(尤指苏格兰的)法定结账日(term day);<法律> 有限期租用的地产;(建筑)界标
v. 把……称为,把……叫做

5. 使用Future以及定义超时

  • 关于返回Future的使用方法

  • 以及对异步执行的超时控制,

定义异步任务

改造主类:主类等子类执行完毕

@Slf4j
@Component
public class Task {

    public static Random random = new Random();

    @Async("taskExecutor")
    public Future<String> run() throws Exception {
        long sleep = random.nextInt(10000);
        log.info("开始任务,需耗时:" + sleep + "毫秒");
        Thread.sleep(sleep);
        log.info("完成任务");
        
        return new AsyncResult<>("test");
    }

}

什么是Future类型?

Future是对于具体的

  • Runnable或者Callable任务的执行结果
  • 进行取消、查询是否完成、获取结果的接口。
  • 必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

它的接口定义如下:

public interface Future<V> {
    
    boolean cancel(boolean mayInterruptIfRunning);
    
    boolean isCancelled();
    
    boolean isDone();
    
    V get() throws InterruptedException, ExecutionException;
    
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

它声明这样的五个方法:

  • cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。
    • 参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。
    • 如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;
    • 如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
  • isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
  • isDone方法表示任务是否已经完成,若任务完成,则返回true;
  • get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
  • get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。

也就是说Future提供了三种功能:

  1. 判断任务是否完成;
  2. 能够中断任务;
  3. 能够获取任务执行结果。

测试执行与定义超时

		Future<String> futureResult = task.run();
		//futureResult.get();主类等子类执行完毕
        String result = futureResult.get(5, TimeUnit.SECONDS);
        log.info(result);

我们在get方法中还定义了该线程执行的超时时间,通过执行这个测试我们可以观察到执行时间超过5秒的时候,这里会抛出超时异常,该执行线程就能够因执行超时而释放回线程池,不至于一直阻塞而占用资源。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值