美文网首页SpringBoot专题springboot
SpringBoot入门建站全系列(二十二)异步任务执行的几种方

SpringBoot入门建站全系列(二十二)异步任务执行的几种方

作者: 逍遥天扬 | 来源:发表于2019-08-15 09:40 被阅读6次

    SpringBoot入门建站全系列(二十二)异步任务执行的几种方式

    一、概述

    异步调用是相对于同步调用而言的,同步调用是指程序按预定顺序一步步执行,每一步必须等到上一步执行完后才能执行,异步调用则无需等待上一步程序执行完即可执行。

    实现异步任务的方式有很多,但是可以总结为多线程异步和多进程异步。

    多线程异步:

    • 多线程实现异步就是新建个线程,将任务交给新线程执行。

    • 不管是自己new Thread实现异步,还是使用ThreadPoolTaskExecutor线程池,还是使用Spring的@EnableAsync注解,这些都是多线程实现的异步。

    多进程异步:

    • 将任务交给另外一个进程处理,已经不在本应用中了。

    • 比如将任务交给MQ,这个就是异步操作了。因为交给MQ以后,你不必等待结果返回。

    • 当然,如果你调用另外一个应用/进程,另外的一个应用/进程将任务加入任务队列,然后立即返回你成功失败,那这个过程也属于异步任务。这个过程就是MQ做的事情了。。

    本篇重点讲述多线程异步任务的执行方式。多进程方式可以查看《SpringBoot入门建站全系列(十七)整合ActiveMq(JMS类消息队列)》《SpringBoot入门建站全系列(十八)整合RabbitMQ(AMQP类消息队列)》中的消息发送方式。

    首发地址:

      品茗IT-同步发布

    品茗IT提供在线支持:

      一键快速构建Spring项目工具

      一键快速构建SpringBoot项目工具

      一键快速构建SpringCloud项目工具

      一站式Springboot项目生成

      Mysql一键生成Mybatis注解Mapper

      Mysql一键生成SpringDataRest项目

    如果大家正在寻找一个java的学习环境,或者在开发中遇到困难,可以加入我们的java学习圈,点击即可加入,共同学习,节约学习时间,减少很多在学习中遇到的难题。

    二、前言

    多线程的异步任务执行有几种方式,还是前面所说的那样,方式虽然不同,但原理是一样的,就是在新线程中执行任务,但是还是要说一下这几种方式。

    • new Thread 普通线程方式执行;

    • ThreadPool线程池,线程使用频繁的时候,减少线程创建回收的开销。实现方式有多种,但是jdk1.5开始支持线程池,就在java.util.concurrent里。

    • Spring提供的@Async注解。其实也是基于线程池。

    • 注意Runnable和Callable在异步任务中的应用,一个不带返回值,一个带返回值而已。

    本文是在Springboot环境中测试的。引入spring-boot-starter-web即可。如果不会搭建,可以打开这篇文章看一看《SpringBoot入门建站全系列(一)项目建立》

    在web项目中测试会比较方便,我们利用RequestContextHolder获取ThreadLocal的Request对象来判断线程是否改变了

    假设我们要执行的任务是这样的, 为了异步执行AsyncTaskService 中的asyncTask方法:

    package com.cff.springbootwork.async.service;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    import org.springframework.web.context.request.RequestAttributes;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    @Service
    public class AsyncTaskService {
        private Logger log = LoggerFactory.getLogger(this.getClass());
    
        /**
         * 普通的一方法而已,供异步任务调用
         */
        public void asyncTask() {
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            if (requestAttributes != null) {
                HttpServletRequest curRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
                log.info("异步任务开始执行,当前请求属性test为:{}", curRequest.getAttribute("test"));
            } else {
                log.info("异步任务不是同一个线程了,别想拿ThreadLocal对象了");
            }
    
            try {
                Thread.sleep(5000);
                log.info("我是异步任务,我就是个打印!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            log.info("5s后异步任务终于执行完成");
        }
    
    }
    
    

    三、普通线程方式

    首先我们在Request中塞入一个值,后面用来获取判断是否是主线程。

    然后用new Thread新建线程执行异步任务。

    我们将逻辑写在AsyncService中,来调用AsyncTaskService的asyncTask方法。

    AsyncService:

    /**
         * 测试new Thread 异步任务
         */
        public void asyncThread() {
            log.info("开始执行任务");
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest();
    
            log.info("请求属性test为:{}", request.getAttribute("test"));
    
            Thread thread1 = new Thread(new Runnable() {
                public void run() {
                    asyncTaskService.asyncTask();
                }
            });
            thread1.start();
    
            HttpServletRequest afterRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest();
            log.info("任务提前执行完成,开始返回,请求属性test为:{}", afterRequest.getAttribute("test"));
        }
    

    结果如下:

    2019-08-08 16:39:50,516 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 开始执行任务
    2019-08-08 16:39:50,517 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 请求属性test为:asdasd
    2019-08-08 16:39:50,522 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 任务提前执行完成,开始返回,请求属性test为:asdasd
    2019-08-08 16:39:50,530 [Thread-4][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 异步任务不是同一个线程了,别想拿ThreadLocal对象了
    2019-08-08 16:39:55,530 [Thread-4][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 我是异步任务,我就是个打印!
    2019-08-08 16:39:55,530 [Thread-4][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 5s后异步任务终于执行完成
    
    • 首先获取下request的test属性,能获取到;

    • 执行异步任务,拿不到request对象了,因为不是同一个线程,ThreadLocal没有。

    • 程序直接返回了,异步任务5s以后才执行完。

    四、线程池方式

    首先我们在Request中塞入一个值,后面用来获取判断是否是主线程。

    然后将异步任务扔给线程池执行。

    这里我们分别使用Runnable和Callable来测试下。

    先建立一个线程池:

    package com.cff.springbootwork.async.service;
    
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class ThreadPoolService {
        private ExecutorService executor;
    
        @PostConstruct
        public void init() {
    //      executor = new ThreadPoolExecutor(20, 30, 60L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(),
    //              new ThreadPoolExecutor.AbortPolicy());
            
            executor = Executors.newCachedThreadPool();
        }
    
        public void execute(Runnable task) {
            executor.execute(task);
        }
    
        public <T> Future<T> submit(Callable<T> task) {
            return executor.submit(task);
        }
    
        @PreDestroy
        public void shutdown() {
            executor.shutdown();
        }
    }
    
    

    4.1 Runnable普通异步任务

    我们将逻辑写在AsyncService中,来调用AsyncTaskService的asyncTask方法。

    AsyncService:

    /**
         * 测试 线程池 异步任务
         */
        public void asyncThreadPool() {
            log.info("开始执行任务");
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest();
    
            log.info("请求属性test为:{}", request.getAttribute("test"));
    
            threadPoolService.execute(new Runnable() {
                public void run() {
                    asyncTaskService.asyncTask();
                }
            });
    
            HttpServletRequest afterRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest();
            log.info("任务提前执行完成,开始返回,请求属性test为:{}", afterRequest.getAttribute("test"));
        }
    

    结果如下:

    2019-08-08 16:44:19,136 [http-nio-8080-exec-4][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 开始执行任务
    2019-08-08 16:44:19,136 [http-nio-8080-exec-4][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 请求属性test为:asdasd
    2019-08-08 16:44:19,137 [http-nio-8080-exec-4][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 任务提前执行完成,开始返回,请求属性test为:asdasd
    2019-08-08 16:44:19,138 [pool-1-thread-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 异步任务不是同一个线程了,别想拿ThreadLocal对象了
    2019-08-08 16:44:24,140 [pool-1-thread-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 我是异步任务,我就是个打印!
    2019-08-08 16:44:24,140 [pool-1-thread-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 5s后异步任务终于执行完成
    
    • 首先获取下request的test属性,能获取到;

    • 执行异步任务,拿不到request对象了,因为不是同一个线程,ThreadLocal没有。

    • 程序直接返回了,异步任务5s以后才执行完。

    和new Thread方式一样。唯一区别就是它是线程池,线程可以回收,线程使用频繁的时候,减少线程创建回收的开销。

    4.2 Callable异步任务返回结果

    我们将逻辑写在AsyncService中,来调用AsyncTaskService的asyncTask方法。

    AsyncService:

    /**
         * 测试 线程池 异步任务 Future回调
         */
        public void asyncFuturePool() {
            log.info("开始执行任务");
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest();
    
            log.info("请求属性test为:{}", request.getAttribute("test"));
    
            Callable<String> callable = new Callable<String>() {
                @Override
                public String call() throws Exception {
                    asyncTaskService.asyncTask();
                    return "1111";
                }
            };
    
            Future<String> future = threadPoolService.submit(callable);
    
            try {
                // future.get(); // 阻塞函数,如果直接用,它就一直阻塞,就不是异步了。
                if (future.isDone()) {
                    log.info("这么快就完成了?不可能!");
                    String result = future.get();
                    log.info("任务结果为:{}", result);
                }
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            } catch (ExecutionException e1) {
                e1.printStackTrace();
            }
    
            HttpServletRequest afterRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest();
            log.info("任务提前执行完成,开始返回,请求属性test为:{}", afterRequest.getAttribute("test"));
        }
    

    如果注掉future.get(),结果如下:

    2019-08-08 16:46:44,001 [http-nio-8080-exec-7][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 开始执行任务
    2019-08-08 16:46:44,002 [http-nio-8080-exec-7][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 请求属性test为:asdasd
    2019-08-08 16:46:44,003 [http-nio-8080-exec-7][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 任务提前执行完成,开始返回,请求属性test为:asdasd
    2019-08-08 16:46:44,003 [pool-1-thread-2][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 异步任务不是同一个线程了,别想拿ThreadLocal对象了
    2019-08-08 16:46:49,003 [pool-1-thread-2][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 我是异步任务,我就是个打印!
    2019-08-08 16:46:49,003 [pool-1-thread-2][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 5s后异步任务终于执行完成
    

    这个过程和4.1完全一样了。

    如果不注掉future.get(),结果如下:

    2019-08-08 16:48:02,472 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 开始执行任务
    2019-08-08 16:48:02,472 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 请求属性test为:asdasd
    2019-08-08 16:48:02,478 [pool-1-thread-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 异步任务不是同一个线程了,别想拿ThreadLocal对象了
    2019-08-08 16:48:07,478 [pool-1-thread-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 我是异步任务,我就是个打印!
    2019-08-08 16:48:07,478 [pool-1-thread-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 5s后异步任务终于执行完成
    2019-08-08 16:48:07,478 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 这么快就完成了?不可能!
    2019-08-08 16:48:07,478 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 任务结果为:1111
    2019-08-08 16:48:07,478 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 任务提前执行完成,开始返回,请求属性test为:asdasd
    

    虽然拿到了结果,也使用了多线程,但是这个过程变成同步了,因为主线程一直等待另外一个线程执行完才执行下一步。

    五、Spring的@Async注解方式

    @Async注解的方法,不能和调用方法在同一个类中,因为它是动态代理调用的。同一个类中动态代理个毛啊。。

    首先要使用注解@EnableAsync开启异步执行功能。

    package com.cff.springbootwork.async.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableAsync;
    
    @Configuration
    @EnableAsync
    public class AsyncConfig {
        
    }
    
    

    5.1 同一个类中测试

    我们将逻辑写在AsyncService中,新建个方法asyncTaskAnnotation,加上@Async注解并调用AsyncTaskService的asyncTask方法,进行测试。

    AsyncService:

    /**
         * 测试 @Async 对异步任务的支持
         */
        @Async
        public void asyncTaskAnnotation() {
            asyncTaskService.asyncTask();
        }
    
        /**
         * 测试 Spring的@Async 对异步任务的支持, 同一个类内的方法不能实现异步
         */
        public void asyncSpringAnnotationOne() {
            log.info("开始执行任务");
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest();
    
            log.info("请求属性test为:{}", request.getAttribute("test"));
    
            asyncTaskAnnotation();
    
            HttpServletRequest afterRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest();
            log.info("任务提前执行完成,开始返回,请求属性test为:{}", afterRequest.getAttribute("test"));
        }
    
    

    测试结果如下:

    2019-08-08 16:54:09,957 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 开始执行任务
    2019-08-08 16:54:09,957 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 请求属性test为:asdasd
    2019-08-08 16:54:09,962 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 异步任务开始执行,当前请求属性test为:asdasd
    2019-08-08 16:54:14,962 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 我是异步任务,我就是个打印!
    2019-08-08 16:54:14,962 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 5s后异步任务终于执行完成
    2019-08-08 16:54:14,962 [http-nio-8080-exec-1][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 任务提前执行完成,开始返回,请求属性test为:asdasd
    

    可以看出,异步任务竟然可以拿到request的test属性了,表明这压根就是同一个线程,@Async无效。

    5.2 不同类中测试

    我们将逻辑写在AsyncService中,在AsyncTaskService新建个方法asyncTaskAnnotation,加上@Async注解并调用asyncTask方法,进行测试。

    AsyncService:

    /**
         * 测试 Spring的@Async 对异步任务的支持, 不同类的方法可以实现异步
         */
        public void asyncSpringAnnotationMuti() {
            log.info("开始执行任务");
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest();
    
            log.info("请求属性test为:{}", request.getAttribute("test"));
    
            asyncTaskService.asyncTaskAnnotation();
    
            HttpServletRequest afterRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .getRequest();
            log.info("任务提前执行完成,开始返回,请求属性test为:{}", afterRequest.getAttribute("test"));
        }
    
    
    

    测试结果如下:

    2019-08-08 16:56:50,964 [http-nio-8080-exec-6][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 开始执行任务
    2019-08-08 16:56:50,964 [http-nio-8080-exec-6][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 请求属性test为:asdasd
    2019-08-08 16:56:50,964 [http-nio-8080-exec-6][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncService] 任务提前执行完成,开始返回,请求属性test为:asdasd
    2019-08-08 16:56:50,965 [SimpleAsyncTaskExecutor-2][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 异步任务不是同一个线程了,别想拿ThreadLocal对象了
    2019-08-08 16:56:55,965 [SimpleAsyncTaskExecutor-2][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 我是异步任务,我就是个打印!
    2019-08-08 16:56:55,965 [SimpleAsyncTaskExecutor-2][IP:|USER:][INFO  com.cff.springbootwork.async.service.AsyncTaskService] 5s后异步任务终于执行完成
    

    可以看出,异步任务拿不到request对象了,@Async正常使用。

    六、 测试类及Service完整代码

    AsyncRest:

    package com.cff.springbootwork.async.web;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.cff.springbootwork.async.service.AsyncService;
    
    /**
     * 测试乐异步任务
     * 
     * @author fufei
     *
     */
    @RestController
    @RequestMapping("/async")
    public class AsyncRest {
    
        @Autowired
        AsyncService asyncService;
    
        @RequestMapping(value = "/thread", method = { RequestMethod.GET })
        public String thread(HttpServletRequest request) {
            request.setAttribute("test", "asdasd");
            asyncService.asyncThread();
            return "0000";
        }
    
        @RequestMapping(value = "/pool", method = { RequestMethod.GET })
        public String pool(HttpServletRequest request) {
            request.setAttribute("test", "asdasd");
            asyncService.asyncThreadPool();
            return "0000";
        }
    
        @RequestMapping(value = "/future", method = { RequestMethod.GET })
        public String future(HttpServletRequest request) {
            request.setAttribute("test", "asdasd");
            asyncService.asyncFuturePool();
            return "0000";
        }
    
        @RequestMapping(value = "/springOne", method = { RequestMethod.GET })
        public String springOne(HttpServletRequest request) {
            request.setAttribute("test", "asdasd");
            asyncService.asyncSpringAnnotationOne();
            return "0000";
        }
    
        @RequestMapping(value = "/springMuti", method = { RequestMethod.GET })
        public String springMuti(HttpServletRequest request) {
            request.setAttribute("test", "asdasd");
            asyncService.asyncSpringAnnotationMuti();
            return "0000";
        }
    }
    
    

    AsyncService:

    
    

    AsyncTaskService:

    
    

    详细完整的代码,可以访问品茗IT-博客《SpringBoot入门建站全系列(二十二)异步任务执行的几种方式》进行查看

    品茗IT-博客专题:https://www.pomit.cn/lecture.html汇总了Spring专题Springboot专题SpringCloud专题web基础配置专题。

    快速构建项目

    Spring组件化构建

    SpringBoot组件化构建

    SpringCloud服务化构建

    喜欢这篇文章么,喜欢就加入我们一起讨论SpringBoot使用吧!


    品茗IT交流群

    相关文章

      网友评论

        本文标题:SpringBoot入门建站全系列(二十二)异步任务执行的几种方

        本文链接:https://www.haomeiwen.com/subject/bqsajctx.html