前言
有了前2篇多线程和线程池基础铺垫后,简单介绍一下多线程在项目中最常见的应用场景。
一、多线程应用
1.项目中哪些业务场景会用到多线程?
业务的并行处理和异步处理。
2. 如何在项目中使用多线程?
并行处理的业务一般会通过Callable和Future创建线程;
异步处理的业务一般会使用@Async注解打在方法上,使得方法异步执行业务。
3. 本篇需要了解多线程和线程池的基础,不清楚的可以去看看我之前的博客:
二、并行业务
1.业务场景
业务
订单详细信息包含了订单相关信息(主订单、子订单)、用户信息、商品信息、物流信息等。
需求
在页面上展示业务详细信息。
分析
该业务场景除了主订单信息,其他订单相关的信息都可以用并行同步查询,最后数据集汇总处理。
2.业务实现
/**
* 用id查询订单表数据
* @param orderId 订单id
* @return
*/
private OrderInfo getOrderById(String orderId){
OrderInfo orderInfo =new OrderInfo();
orderInfo.setUserId("111");
orderInfo.setOrderId(orderId);
orderInfo.setGoodId("222");
//模拟查询数据库业务 200ms
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return orderInfo;
}
/**
* 查询用户信息
* @param userId 用户id
* @return
*/
private String getUserInfo(String userId){
//模拟rpc查询用户数据 900ms
try {
Thread.sleep(900);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "张三";
}
/**
* 查询商品信息
* @param goodsId 商品id
* @return
*/
private String getGoodsInfo(String goodsId){
//模拟rpc查询商品数据 2000ms
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "苹果";
}
/**
* 获取订单数据
* @param orderId 订单id
* @return
*/
public OrderInfo getOrderInfo(String orderId){
//开始时间
long startTime = System.currentTimeMillis();
log.info("查询订单数据,订单id:{}",orderId);
//获取到订单数据
OrderInfo orderInfo =getOrderById(orderId);
//订单数据不能为空
if(StringUtils.isEmpty(orderInfo)){
return orderInfo;
}
log.info("获取到订单表数据为:{},接口耗时为:{}",orderInfo.toString(),System.currentTimeMillis()-startTime);
//创建一个核心线程数10,最大线程容量20 ,60s存活,有界容量是20的队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,20,
60,TimeUnit.SECONDS,new ArrayBlockingQueue<>(20));
//查询订单的用户相关信息
Future<String> userFuture = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
//获取用户信息
return getUserInfo(orderInfo.getUserId());
}
});
//查询订单的商品相关信息
Future<String> goodsFuture = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
//获取用户信息
return getGoodsInfo(orderInfo.getGoodId());
}
});
try {
//用户信息
String userName = userFuture.get();
orderInfo.setUserName(userName);
//商品信息
String goodName = goodsFuture.get();
orderInfo.setGoodName(goodName);
} catch (Exception e){
//其他处理异常逻辑
log.error("查询订单数据异常,订单id:{}",orderId,e);
} finally {
//关闭线程池
executor.shutdown();
}
log.info("订单的详细信息:{},接口耗时为:{} ms",orderInfo.toString(),System.currentTimeMillis()-startTime);
return orderInfo;
}
3.代码测试
4.结果分析
以上的demo,获取订单表数据时间是200ms,获取用户数据时间是900ms,获取商品数据时间是1000ms,
如果是同步执行则最终时间的应该是 200+900+1000 = 2100ms ,而用户和商品数据采用的多线程执行,
会并行执行消耗时间为线程最耗时的线程(查询商品线程 1000ms),则最后的耗时是 200+1000=1200ms,这就是并行处理的好处。
从以上的分析可以得出 Future.get()是一个同步则塞的方法。
5.线程池建议
由于在大流量下使用Executors静态方法创建的线程池存在OOM或CPU占用高的问题,则阿里巴巴开发手册已禁用了使用Executors方式创建线程池。
推荐尽量使用ThreadPoolExecutor手动创建线程池,具体可以参考我之前关于线程池的博客。
三、异步业务
1.业务场景
业务需求
订单报表的数据,支持1年内数据的excel导出的功能。
分析
大数据的excel导出非常慢,如果同步处理则接口一定会超时,且会在导出界面一直loading也会影响用户的体验,
则建议导出功能做成异步在后台生成好excel报表并将其上传到本地FastDFS文件服务器上,待上传成功后更新用户导出记录表的状态和url,用户可以在他的导出记录页面下载他要导出的报表。
2.业务实现
// 自定义@Async的线程池
@EnableAsync
@Configuration
public class TaskPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程数 10
executor.setCorePoolSize(10);
//线程池最大的容量数
executor.setMaxPoolSize(20);
//阻塞队列的容量
executor.setQueueCapacity(100);
//非核心线程存活时间
executor.setKeepAliveSeconds(60);
//自定义线程名称
executor.setThreadNamePrefix("taskExecutor-");
//默认拒绝策略 超过处理量则直接抛错
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
return executor;
}
}
//业务类
@Component
@Slf4j
public class OrderService {
/**
* 导出报表的业务
*/
@Async("taskExecutor") //此处必须要使用自定义线程池的beanName
public void exportExcel(){
log.info("后台开始处理导出报表的业务,线程名:{}",Thread.currentThread().getName());
long start = System.currentTimeMillis();
//1.添加用户下载记录表初始数据
//2.生成订单报表数据
//3.将报表上传到FastDFS上
//4.报表上传成功后更新用户下载记录的状态和url
try {
//模拟处理以上业务的耗时
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("后台处理报表成功,耗时:" + (System.currentTimeMillis() - start) + "ms,线程名:"+Thread.currentThread().getName());
}
}
//控制层
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/export")
public String exportExcel() {
log.info("用户执行了导出报表功能。。。线程名:{}",Thread.currentThread().getName());
long start = System.currentTimeMillis();
//异步导出报表数据
orderService.exportExcel();
log.info("在后台处理导出业务,并响应给用户 ,耗时为:{} ms,线程名:{}",System.currentTimeMillis()-start,Thread.currentThread().getName());
return "ok";
}
}
3.代码测试
1. 主线程 http-nio-8080-exec-2 耗时6ms就直接响应用户了。
2. 业务线程 taskExecutor-1 在后台处理导出业务 耗时 3000ms就完成相关业务
4.小结
1. 方法使用了@Async 会另起一个线程来处理该方法的业务,从而起到异步处理的效果;
2. 推荐为@Async自定义线程池,如果不自定义线程池,则每一次调用该方法的开销相对于new Thread(),
开启线程需要占用一定的内存空间,(默认情况下,主线程1M,子线程512KB),如果在大流量下会直接导致程序OOM.
这个也是大多数程序猿忽略的细节。
3. 推荐给自定义的线程池定义线程的名称,以便后期在日志中定位问题。