一、楔子
在我们的系统中,经常会处理一些耗时任务,自然而然的会想到使用多线程,JDK给我们提供了非常方便的操作线程的API,为什么还要使用Spring来实现多线程呢?
1.使用Spring比使用JDK原生的并发API更简单。(一个注解@Async就搞定)
2.我们的应用环境一般都会集成Spring,我们的Bean也都交给Spring来进行管理,那么使用Spring来实现多线程更加简单,更加优雅。
为什么要用异步?当需要调用多个服务时,使用传统的同步调用来执行时,是这样的
如果每个服务需要3秒的响应时间,这样顺序执行下来,可能需要9秒以上才能完成业务逻辑,但是如果我们使用异步调用
调用服务A
调用服务B
调用服务C
然后等待从服务A、B和C的响应
根据从服务A、服务B和服务C返回的数据完成业务逻辑,然后结束
理论上 3秒左右即可完成同样的业务逻辑
二、spring boot 如何使用多线程
Spring中实现多线程,其实非常简单,只需要在配置类中添加@EnableAsync就可以使用多线程。在希望执行的并发方法中使用@Async就可以定义一个线程任务。通过spring给我们提供的ThreadPoolTaskExecutor就可以使用线程池。
2.1 第一步,先在Spring Boot主类中定义一个线程池
package com.godfreyy.springbootasync.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* 异步任务配置类
*
* @author godfrey
* @since 2021-12-16
*/
@Configuration
@EnableAsync // 启用异步任务
public class AsyncConfiguration {
/**
* 声明一个线程池(并指定线程池的名字)
*
* @param
* @return Executor
*/
@Bean("taskExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程数3:线程池创建时候初始化的线程数
executor.setCorePoolSize(3);
//最大线程数5:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
executor.setMaxPoolSize(5);
//缓冲队列500:用来缓冲执行任务的队列
executor.setQueueCapacity(500);
//允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(60);
//线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("DailyAsync-");
executor.initialize();
return executor;
}
}
有很多你可以配置的东西。默认情况下,使用SimpleAsyncTaskExecutor。
2.2 第二步,使用线程池
在定义了线程池之后,我们如何让异步调用的执行任务使用这个线程池中的资源来运行呢?方法非常简单,我们只需要在@Async注解中指定线程池名即可,比如:
package com.godfreyy.springbootasync.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* @author godfrey
* @since 2021-12-16
*/
@Service
public class GitHubLookupService {
private static final Logger logger = LoggerFactory.getLogger(GitHubLookupService.class);
@Resource
private RestTemplate restTemplate;
// 这里进行标注为异步任务,在执行此方法的时候,会单独开启线程来执行(并指定线程池的名字)
@Async("taskExecutor")
public CompletableFuture<String> findUser(String user) throws InterruptedException {
logger.info("Looking up " + user);
String url = String.format("https://api.github.com/users/%s", user);
String results = restTemplate.getForObject(url, String.class);
// Artificial delay of 3s for demonstration purposes
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("sleep...");
} catch (InterruptedException e) {
e.printStackTrace();
}
return CompletableFuture.completedFuture(results);
}
}
findUser 方法被标记为Spring的 @Async 注解,表示它将在一个单独的线程上运行。该方法的返回类型是 CompleetableFuture 而不是 String,这是任何异步服务的要求。
2.3 第三步,单元测试
最后,我们来写个单元测试来验证一下
package com.godfreyy.springbootasync;
import com.godfreyy.springbootasync.service.GitHubLookupService;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@SpringBootTest
class SpringbootAsyncApplicationTests {
private static final Logger logger = LoggerFactory.getLogger(SpringbootAsyncApplicationTests.class);
@Resource
private GitHubLookupService gitHubLookupService;
@Test
public void asyncTest() throws InterruptedException, ExecutionException {
// Start the clock
long start = System.currentTimeMillis();
// Kick of multiple, asynchronous lookups
CompletableFuture<String> page1 = gitHubLookupService.findUser("PivotalSoftware");
CompletableFuture<String> page2 = gitHubLookupService.findUser("CloudFoundry");
CompletableFuture<String> page3 = gitHubLookupService.findUser("Spring-Projects");
// Wait until they are all done
//join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行
CompletableFuture.allOf(page1, page2, page3).join();
// Print results, including elapsed time
float exc = (float)(System.currentTimeMillis() - start)/1000;
logger.info("Elapsed time: " + exc + " seconds");
logger.info("--> " + page1.get());
logger.info("--> " + page2.get());
logger.info("--> " + page3.get());
}
}
执行上面的单元测试,我们可以在控制台中看到所有输出的线程名前都是之前我们定义的线程池前缀名开始的,并且执行时间小于9秒,说明我们使用线程池来执行异步任务的试验成功了!
三、注意事项
在使用spring的异步多线程时经常回碰到多线程失效的问题,解决方式为:
异步方法和调用方法一定要写在不同的类中 ,如果写在一个类中,是没有效果的!
原因:
spring对@Transactional注解时也有类似问题,spring扫描时具有@Transactional注解方法的类时,是生成一个代理类,由代理类去开启关闭事务,而在同一个类中,方法调用是在类体内执行的,spring无法截获这个方法调用。