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注解

java 复制代码
@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注解

java 复制代码
@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、查看调用

java 复制代码
@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注解,输出如下:【可见是串行执行】

java 复制代码
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注解,输出如下:【可见是并行执行】

java 复制代码
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方式

java 复制代码
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)。新增如下配置类:

java 复制代码
@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的初始化配置,还有很多种,可以参看www.cnblogs.com/bjlhx/categ...

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

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

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

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

java 复制代码
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重新被调用,将结果返回给用户;

代码实现示例如下:

java 复制代码
@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接口来完成线程池的配置。

java 复制代码
@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去获取结果。

修改异步执行的方法

java 复制代码
@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");
    }
}

修改调用的方法

java 复制代码
@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实现类

java 复制代码
@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);
        }
    }
}

调用方法

java 复制代码
@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实体类

java 复制代码
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

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

    private String name;

    private String blog;

}

CompletableFuture的服务

java 复制代码
@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);
    }
}

调用方法

java 复制代码
@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的包装,提供了更强大的功能,比如:处理超时回调、错误回调、完成回调等。

java 复制代码
@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的使用先来看一下官方的例子和说明:

java 复制代码
@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<>();
    // Save the deferredResult in in-memory queue ...
    return deferredResult;
}

@RequestMapping("/getQuotes")
@ResponseBody
public DeferredResult<String> getQuotes() {
    // 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、定时任务或其他线程触发。来个实例:

java 复制代码
@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包中,而WebAsyncTaskDeferredResult是用于Web(Spring MVC)的,而Callable是用于concurrent(并发)处理的。

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

java 复制代码
// 普通方法
public String aMethod(){
}

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

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

  • DeferredResult使用方式与Callable类似,重点在于跨线程之间的通信。
  • @Async也是替换Runnable的一种方式,可以代替我们自己创建线程。而且适用的范围更广,并不局限于Controller层,而可以是任何层的方法上。
相关推荐
爱勇宝8 小时前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
IT_陈寒13 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码14 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo14 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志15 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..15 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects15 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰15 小时前
一些基础和问题解决
后端
陈随易15 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN15 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp