SpringBoot异步调用

在程序执行时候还有一个瓶颈,串行执行,可以通过使用不同线程类快速提升应用的速度。

要启用Spring的异步功能,必须要使用@EnableAsync注解。这样将会透明地使用java.util.concurrent.Executor来执行所有带有@Async注解的方法。

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

针对调用的Async,如果不做Future特殊处理,执行完调用方法会立即返回结果,如异步邮件发送,不会真的等邮件发送完毕才响应客户,如需等待可以使用Future阻塞处理。

一、异步实现 - EnableAsync

SpringBoot中将一个方法声明为异步方法非常简单,只需两个注解即可@EnableAsync@Async

  • @EnableAsync:用于开启SpringBoot支持异步的功能,用在SpringBoot的启动类上。
  • @Async:用于方法上,标记该方法为异步处理方法。

需要注意的是@Async并不支持用于被@Configuration注解的类的方法上。同一个类中,一个方法调用另外一个有@Async的方法,注解也是不会生效的。

1.1 使用示例

1、开启异步功能

main方法增加@EnableAsync注解

@SpringBootApplication
@EnableAsync
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        System.out.println("ThreadId:" + Thread.currentThread().getId());
    }
}

2、异步方法

在所需方法增加@Async注解

@Component
public class Task {
    @Async
    public void doTaskOne() throws Exception {
        for (int i = 0; i < 3; i++) {
            Thread.sleep(200);
            System.out.println("ThreadId:" + Thread.currentThread().getId() + ":doTaskOne");
        }
    }

    @Async
    public void doTaskTwo() throws Exception {
        for (int i = 0; i < 3; i++) {
            Thread.sleep(200);
            System.out.println("ThreadId:" + Thread.currentThread().getId() + ":doTaskTwo");
        }
    }

    @Async
    public void doTaskThree() throws Exception {
        for (int i = 0; i < 3; i++) {
            Thread.sleep(200);
            System.out.println("ThreadId:" + Thread.currentThread().getId() + ":doTaskThree");
        }
    }
}

@Async注解的使用与Callable有类似之处,在默认情况下使用的都是SimpleAsyncTaskExecutor线程池,可参考Callable中的方式来自定义线程池。

3、查看调用

@RestController
@RequiredArgsConstructor
public class TestAsyns {

    private final Task task;

    @RequestMapping("/testAsync")
    public ResponseEntity testAsync() throws Exception {
        task.doTaskOne();
        task.doTaskTwo();
        task.doTaskThree();
        return ResponseEntity.ok("ok");
    }
}

上述方法依次调用三个方法。

如果去除@EnableAsync注解,输出如下:【可见是串行执行】

ThreadId:33:doTaskOne
ThreadId:33:doTaskOne
ThreadId:33:doTaskOne
ThreadId:33:doTaskTwo
ThreadId:33:doTaskTwo
ThreadId:33:doTaskTwo
ThreadId:33:doTaskThree
ThreadId:33:doTaskThree
ThreadId:33:doTaskThree

如果增加@EnableAsync注解,输出如下:【可见是并行执行】

ThreadId:56:doTaskThree
ThreadId:55:doTaskTwo
ThreadId:54:doTaskOne
ThreadId:54:doTaskOne
ThreadId:55:doTaskTwo
ThreadId:56:doTaskThree
ThreadId:54:doTaskOne
ThreadId:56:doTaskThree
ThreadId:55:doTaskTwo

二、自定义执行器使用异步

2.1 注入Bean方式

import java.util.concurrent.Executor;  
  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.ComponentScan;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.scheduling.annotation.EnableAsync;  
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;  
  
@Configuration  
public class ThreadConfig  {  
  
     // 执行需要依赖线程池,这里就来配置一个线程池  
     @Bean  
     public Executor getExecutor() {  
          ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
          executor.setCorePoolSize(5);  
          executor.setMaxPoolSize(10);  
          executor.setQueueCapacity(25);  
          executor.initialize();  
          return executor;  
     }  
}

2.2 实现AsyncConfigurer接口

通过实现AsyncConfigurer接口,可以自定义默认的执行(executor)。新增如下配置类:

@Configuration
@Slf4j
public class AsyncConfiguration implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        //做好不超过10个,这里写两个方便测试
        return Executors.newFixedThreadPool(2);
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex,method,params) -> log.error("Uncaught async error", ex);
    }
}

Executor的初始化配置,还有很多种,可以参看https://www.cnblogs.com/bjlhx/category/1086008.html

Spring已经实现的异步线程池:

  1. SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
  2. SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
  3. ConcurrentTaskExecutor(不推荐):Executor的适配类。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
  4. SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类。
  5. ThreadPoolTaskExecutor(推荐):最常使用,其实质是对java.util.concurrent.ThreadPoolExecutor的包装。

使用上述配置,能够确保在应用中,用来处理异步任务的线程不会超过10个。这对于web应用很重要,因为每个客户端都会有一个专用的线程。你所使用的线程越多,阻塞时间越长那么能够处理的客户端就会越少。

如果设置成两个,程序中有3个异步线程,也会只有两个运行,如下:

ThreadId:55:doTaskTwo
ThreadId:54:doTaskOne
ThreadId:55:doTaskTwo
ThreadId:54:doTaskOne
ThreadId:55:doTaskTwo
ThreadId:54:doTaskOne
ThreadId:55:doTaskThree
ThreadId:55:doTaskThree
ThreadId:55:doTaskThree

三、异步返回处理

3.1 Callable

对于一次请求(/email),基于Callable的处理流程如下:

  1. Spring MVC开启副线程处理业务(将Callable提交到TaskExecutor);
  2. DispatcherServlet和所有的Filter退出Web容器的线程,但是response保持打开状态;
  3. Callable返回结果,SpringMVC将原始请求重新派发给容器(再重新请求一次/email),恢复之前的处理;
  4. DispatcherServlet重新被调用,将结果返回给用户;

代码实现示例如下:

@GetMapping("/email")
public Callable<String> order() {
    System.out.println("主线程开始:" + Thread.currentThread().getName());
    Callable<String> result = () -> {
        System.out.println("副线程开始:" + Thread.currentThread().getName());
        Thread.sleep(1000);
        System.out.println("副线程返回:" + Thread.currentThread().getName());
        return "success";
    };

    System.out.println("主线程返回:" + Thread.currentThread().getName());
    return result;
}

访问对应URL,控制台输入日志如下:

主线程开始:http-nio-8080-exec-1
主线程返回:http-nio-8080-exec-1
副线程开始:task-1
副线程返回:task-1

通过日志可以看出,主线程已经完成了,副线程才进行执行。同时,URL返回结果“success”。这也说明一个问题,服务器端的异步处理对客户端来说是不可见的。

Callable默认使用SimpleAsyncTaskExecutor类来执行,这个类非常简单而且没有重用线程。在实践中,需要使用AsyncTaskExecutor类来对线程进行配置。

这里通过实现WebMvcConfigurer接口来完成线程池的配置。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Resource
    private ThreadPoolTaskExecutor myThreadPoolTaskExecutor;

    /**
     * 配置线程池
     */
    @Bean(name = "asyncPoolTaskExecutor")
    public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(2);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.setKeepAliveSeconds(200);
        taskExecutor.setThreadNamePrefix("thread-pool-");
        // 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }

    @Override
    public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
        // 处理callable超时
        configurer.setDefaultTimeout(60 * 1000);
        configurer.setTaskExecutor(myThreadPoolTaskExecutor);
        configurer.registerCallableInterceptors(timeoutCallableProcessingInterceptor());
    }

    @Bean
    public TimeoutCallableProcessingInterceptor timeoutCallableProcessingInterceptor() {
        return new TimeoutCallableProcessingInterceptor();
    }
}

3.2 使用Future + 轮询(不推荐)

jdk1.5产物,没有提供Callback机制,只能主动轮询,通过get去获取结果。

修改异步执行的方法

@Component
public class TaskFutureDemo {

    @Async
    public Future<String> doTaskOne() throws Exception {
        for (int i = 0; i < 3; i++) {
            Thread.sleep(1000);
            System.out.println("ThreadId:" + Thread.currentThread().getId() + ":doTaskOne");
        }
        return new AsyncResult<>("doTaskOne");
    }

    @Async
    public Future<String> doTaskTwo() throws Exception {
        for (int i = 0; i < 3; i++) {
            Thread.sleep(1000);
            System.out.println("ThreadId:" + Thread.currentThread().getId() + ":doTaskTwo");
        }
        return new AsyncResult<>("doTaskTwo");
    }

    @Async
    public Future<String> doTaskThree() throws Exception {
        for (int i = 0; i < 3; i++) {
            Thread.sleep(1000);
            System.out.println("ThreadId:" + Thread.currentThread().getId() + ":doTaskThree");
        }
        return new AsyncResult<>("doTaskThree");
    }
}

修改调用的方法

@RestController
@RequiredArgsConstructor
public class TestAsynsFutureController {

    private final TaskFutureDemo task;

    @RequestMapping("/testAsyncFuture")
    public ResponseEntity testAsyncFuture() throws Exception {
        Future<String> taskOne = task.doTaskOne();
        Future<String> taskTwo = task.doTaskTwo();
        Future<String> taskThree = task.doTaskThree();
        while (true) {
            if (taskOne.isDone() && taskTwo.isDone() && taskThree.isDone()) {
                break;
            }
        }
        return ResponseEntity.ok("ok");
    }
}

3.3 Spring的ListenableFuture和CountDownLatch处理

Service实现类

@Service
@RequiredArgsConstructor
public class TaskListenableFutureService {

    private final AsyncSearch asyncSearch;

    public List<String> search(List<String> keywords) {
        CountDownLatch latch = new CountDownLatch(keywords.size());
        List<String> allResult = Collections.synchronizedList(new ArrayList<>());
        keywords.stream().forEach(keyword -> asyncFetch(latch, allResult, keyword));
        await(latch);
        return allResult;
    }

    private void asyncFetch(CountDownLatch latch, List<String> result, String keyword) {
        asyncSearch.asyncFetch(keyword)
                .addCallback(
                        key->onSuccess(result, latch, key),
                        ex -> onError(latch, ex)
                );
    }

    private void await(CountDownLatch latch) {
        try {
            latch.await();
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }

    private static void onSuccess(List<String> result, CountDownLatch latch, String keyword) {
        result.add(keyword);
        latch.countDown();
    }

    private static void onError(CountDownLatch latch, Throwable ex) {
        ex.printStackTrace();
        latch.countDown();
    }

    @Component
    @Slf4j
    private static class AsyncSearch {

        @Autowired
        public AsyncSearch() {
        }

        @Async
        public ListenableFuture<String> asyncFetch(String keyword) {
            log.info(Thread.currentThread().getName() + "-" + keyword);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return new AsyncResult<>("keyword="+keyword);
        }
    }
}

调用方法

@RestController
@Slf4j
@RequiredArgsConstructor
public class TestAsynsListenableFutureController {

    private final TaskListenableFutureService task;

    @RequestMapping("/testAsyncListenableFuture")
    public ResponseEntity testAsyncListenableFuture() throws Exception {

        List<String> list = task.search(Arrays.asList("java", "html", "spring"));
        list.stream().forEach(p -> log.info(p));

        return ResponseEntity.ok("ok");
    }
}

3.4 使用CompletableFuture(推荐)

User实体类

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown=true)
@Data
public class User {

    private String name;

    private String blog;

}

CompletableFuture的服务

@Service
@Slf4j
@RequiredArgsConstructor
public class GitHubLookupService {

    private final RestTemplate restTemplate;

    @Async
    public CompletableFuture<User> findUser(String user) throws InterruptedException {
        log.info("Looking up " + user);
        String url = String.format("https://api.github.com/users/%s", user);
        User results = restTemplate.getForObject(url, User.class);
        // Artificial delay of 1s for demonstration purposes
        Thread.sleep(1000L);
        return CompletableFuture.completedFuture(results);
    }
}

调用方法

@RestController
@Slf4j
@RequiredArgsConstructor
public class CompletableFutureController {

    private final GitHubLookupService gitHubLookupService;

    @RequestMapping("/testCompletableFuture")
    public ResponseEntity testCompletableFuture() throws Exception {
        long start = System.currentTimeMillis();

        CompletableFuture<User> page1 = gitHubLookupService.findUser("PivotalSoftware");
        CompletableFuture<User> page2 = gitHubLookupService.findUser("CloudFoundry");
        CompletableFuture<User> page3 = gitHubLookupService.findUser("Spring-Projects");

        CompletableFuture.allOf(page1, page2, page3).join();

        log.info("Elapsed time: " + (System.currentTimeMillis() - start));
        log.info("--> " + page1.get());
        log.info("--> " + page2.get());
        log.info("--> " + page3.get());

        return ResponseEntity.ok("ok");
    }
}

四、基于WebAsyncTask实现

Spring提供的WebAsyncTask是对Callable的包装,提供了更强大的功能,比如:处理超时回调、错误回调、完成回调等。

@GetMapping("/webAsyncTask")
public WebAsyncTask<String> webAsyncTask() {

    log.info("外部线程:" + Thread.currentThread().getName());
    WebAsyncTask<String> result = new WebAsyncTask<>(60 * 1000L, new Callable<String>() {
        @Override
        public String call() {
            log.info("内部线程:" + Thread.currentThread().getName());
            return "success";
        }
    });

    result.onTimeout(new Callable<String>() {
        @Override
        public String call() {
            log.info("timeout callback");
            return "timeout callback";
        }
    });

    result.onCompletion(new Runnable() {
        @Override
        public void run() {
            log.info("finish callback");
        }
    });
    return result;
}

访问对应请求,打印日志:

2021-02-21 10:22:33.028 INFO 8547 --- [nio-8080-exec-1] c.s.learn.controller.AsyncController : 外部线程:http-nio-8080-exec-1
2021-02-21 10:22:33.033 INFO 8547 --- [ thread-pool-1] c.s.learn.controller.AsyncController : 内部线程:thread-pool-1
2021-02-21 10:22:33.055 INFO 8547 --- [nio-8080-exec-2] c.s.learn.controller.AsyncController : finish callback

五、基于DeferredResult实现

DeferredResult使用方式与Callable类似,但在返回结果时不一样,它返回的实际结果可能没有生成,实际的结果可能会在另外的线程里面设置到DeferredResult中去。

DeferredResult的这个特性对实现服务端推技术、订单过期时间处理、长轮询、模拟MQ的功能等高级应用非常重要。

关于DeferredResult的使用先来看一下官方的例子和说明:

@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {

  DeferredResult<String> deferredResult = new DeferredResult<String>();
  // Save the deferredResult in in-memory queue ...
  return deferredResult;
}

// In some other thread...
deferredResult.setResult(data);

上述示例中我们可以发现DeferredResult的调用并不一定在Spring MVC当中,它可以是别的线程。官方的解释也是如此:

In this case the return value will also be produced from a separate thread. However, that thread is not known to Spring MVC. For example the result may be produced in response to some external event such as a JMS message, a scheduled task, etc.

也就是说,DeferredResult返回的结果也可能是由MQ、定时任务或其他线程触发。来个实例:

@Controller
@RequestMapping("/async/controller")
public class AsyncHelloController {

    private List<DeferredResult<String>> deferredResultList = new ArrayList<>();

    @ResponseBody
    @GetMapping("/hello")
    public DeferredResult<String> helloGet() throws Exception {
        DeferredResult<String> deferredResult = new DeferredResult<>();

        //先存起来,等待触发
        deferredResultList.add(deferredResult);
        return deferredResult;
    }

    @ResponseBody
    @GetMapping("/setHelloToAll")
    public void helloSet() throws Exception {
        // 让所有hold住的请求给与响应
        deferredResultList.forEach(d -> d.setResult("say hello to all"));
    }
}

第一个请求/hello,会先将deferredResult存起来,前端页面是一直等待(转圈)状态。直到发第二个请求:setHelloToAll,所有的相关页面才会有响应。

整个执行流程如下:

  • controller返回一个DeferredResult,把它保存到内存里或者List里面(供后续访问);
  • Spring MVC调用request.startAsync(),开启异步处理;与此同时将DispatcherServlet里的拦截器、Filter等等都马上退出主线程,但是response仍然保持打开的状态;
  • 应用通过另外一个线程(可能是MQ消息、定时任务等)给DeferredResult#setResult值。然后SpringMVC会把这个请求再次派发给servlet容器;
  • DispatcherServlet再次被调用,然后处理后续的标准流程;

通过上述流程可以发现:利用DeferredResult可实现一些长连接的功能,比如当某个操作是异步时,可以先保存对应的DeferredResult对象,当异步通知回来时,再找到这个DeferredResult对象,在setResult处理结果即可。从而提高性能。

六、拓展

6.1 @Async、WebAsyncTask、Callable、DeferredResult的区别

所在的包不同:

  • @Async:org.springframework.scheduling.annotation;
  • WebAsyncTask:org.springframework.web.context.request.async;
  • Callable:java.util.concurrent;
  • DeferredResult:org.springframework.web.context.request.async;

通过所在的包,我们应该隐隐约约感到一些区别,比如@Async是位于scheduling包中,而WebAsyncTask和DeferredResult是用于Web(Spring MVC)的,而Callable是用于concurrent(并发)处理的。

对于Callable,通常用于Controller方法的异步请求,当然也可以用于替换Runnable的方式。在方法的返回上与正常的方法有所区别:

// 普通方法
public String aMethod(){
}

// 对照Callable方法
public Callable<String> aMethod(){
}

而WebAsyncTask是对Callable的封装,提供了一些事件回调的处理,本质上区别不大。

DeferredResult使用方式与Callable类似,重点在于跨线程之间的通信。

@Async也是替换Runable的一种方式,可以代替我们自己创建线程。而且适用的范围更广,并不局限于Controller层,而可以是任何层的方法上。

当然,大家也可以从返回结果,异常处理等角度来分析一下,这里就不再展开了。

https://www.cnblogs.com/bjlhx/p/10364385.html

https://mp.weixin.qq.com/s/bFopVr64NVcnv1SRuHIuIw

posted @ 2022-10-11 18:04  夏尔_717  阅读(349)  评论(0编辑  收藏  举报