多线程项目实战上篇

前言

有了前2篇多线程和线程池基础铺垫后,简单介绍一下多线程在项目中最常见的应用场景。


一、多线程应用

	1.项目中哪些业务场景会用到多线程?
		业务的并行处理和异步处理。
	2. 如何在项目中使用多线程?
		并行处理的业务一般会通过CallableFuture创建线程;
		异步处理的业务一般会使用@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. 推荐给自定义的线程池定义线程的名称,以便后期在日志中定位问题。
  • 9
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ariel小葵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值