多线程在项目中的应用

1.发短信
发短信的场景有很多,比如手机号+验证码登录注册,电影票买完之后会发送取票码,发货之后会有物流信息,支付之后银行发的付款信息,电力系统的电费预警信息等等

在这些业务场景中,有一个特征,那就是主业务可以和短信业务割裂,比如手机号+验证码登陆,当我们点击获取验证码的时候,会连接短信业务平台发送短信,但是发短信这个业务受到短信平台的影响,可能会存在一定时间的延时,但是我们不一定非要等短信平台返回之后,再给用户返回,我们可以先返回获取验证码成功的提升样式,将发短信的业务放入到另外一个线程中执行,用户晚一会收到短信对整体的业务流程也不会受到影响,反而提升了用户体验

代码演示:
1.在springboot项目中导入依赖:

        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-lang3artifactId>
        dependency>

        <dependency>
            <groupId>commons-codecgroupId>
            <artifactId>commons-codecartifactId>
        dependency>
  1. 编写自定义线程池配置
@Configuration
@EnableAsync  // 开启异步注解支持
@Slf4j
public class ExecutorConfig {

    @Bean("asyncServiceExecutor")
    public Executor asyncServiceExecutor(){
        log.info("start asyncServiceExecutor");
           // 这里使用的是任务类型的线程池
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
           // 配置核心线程数
        executor.setCorePoolSize(5);
           // 配置最大线程数
        executor.setMaxPoolSize(5);
            // 配置阻塞队列大小
        executor.setQueueCapacity(5);
            // 配置线程池中线程名称的前缀
        executor.setThreadNamePrefix("async-service-");
          // rejection-policy: 当线程池已经达到最大max-size时,如何处理新任务
          // CALL-RUNS: 不在新线程中执行任务,而是在调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
          // 执行初始化
        executor.initialize();

        return executor;

    }
}

3.线程池要执行的任务

@Service
public class ThreadService {

    @Async("asyncServiceExecutor") // 此处代表使用asyncServiceExecutor这个线程池来执行下面这个方法
    public void send(String phone,int code){
        // 此处模拟发短信业务耗时5秒
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("发短信成功:"+code);
    }

}

4.测试代码

@Service
public class SmsService {

    @Autowired
    private ThreadService threadService;


    public String send(String phone) {
        /**
         *  1.生成验证码
         *  2.调用短信平台发送验证码(发入线程池)
         *  3.将验证码放入redis
         */
        int code = RandomUtils.nextInt(100000, 999999);
        // 调用短信平台发送短信业务
        // 放入到线程池中执行
        threadService.send(phone,code); // 那摩此处就是异步执行的,主线程不会等这一步执行完再执行
        System.out.println("主线程继续执行业务");
        return "success";
    }
}

2.推送
比如有一个业务场景:
有一个审核业务,当收到数据之后,需要将这些数据发送给第三方的监管系统进行审核,数据量有百万之多,一条数据按照一秒计算,那摩需要经过百万秒,200多个小时才能处理完

解决:
考虑引入多线程进行并发操作,降低数据推送时间,提供数据推送的实时性
要注意的问题:

  • 防止重复推送
    可以考虑将数据切分成不同的数据段,每一个线程负责一个
  • 失败处理
    推送失败后,进行失败推送的数据记录,用额外的程序处理失败数据(补偿措施)

代码演示:

  1. 同样这里使用自定义线程池
@Configuration
@EnableAsync  // 开启异步注解支持
@Slf4j
public class ExecutorConfig {

    @Bean("asyncServiceExecutor")
    public ThreadPoolTaskExecutor asyncServiceExecutor(){
        log.info("start asyncServiceExecutor");
           // 这里使用的是任务类型的线程池
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
           // 配置核心线程数
        executor.setCorePoolSize(5);
           // 配置最大线程数
        executor.setMaxPoolSize(5);
            // 配置阻塞队列大小
        executor.setQueueCapacity(5);
            // 配置线程池中线程名称的前缀
        executor.setThreadNamePrefix("async-service-");
          // rejection-policy: 当线程池已经达到最大max-size时,如何处理新任务
          // CALL-RUNS: 不在新线程中执行任务,而是在调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
          // 执行初始化
        executor.initialize();

        return executor;

    }

2.使用线程池来执行该任务

 @Service
public class ThreadService {
@Async("asyncServiceExecutor") // 此处代表使用asyncServiceExecutor这个线程池来执行下面这个方法
    public void push(int[] array,int start,int end){
        long s = System.currentTimeMillis();
        for (int i = start;i<=end;i++){
            pushSend(array[i]);
            // 推送失败,可以记录日志
        }
        long e = System.currentTimeMillis();
        System.out.println((e-s)+"ms");
    }


    private void pushSend(int i) {
        try {
            // 此处模拟推送到第三方平台需要耗时1毫秒
            TimeUnit.MILLISECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3.推送消息

@Service
public class PushService {
@Autowired
    private ThreadService threadService;
    
    /**
     * 新的推送方式
     */
    public void pushNew(){
        // 假设我们要推送一万条数据
        int dataNum = 10000;
        int[] array = new int[dataNum];
        for (int i = 0; i < dataNum; i++) {
            array[i] = i;
        }

        long start = System.currentTimeMillis();

        /**
         * 要使用线程池了,线程池有10个线程,一个线程执行1000条数据
         * 防止重复推送,要把1万条数据做一个拆分
         */
        for (int i = 0; i < 10; i++) {
            int s = i * 1000;
            int e = i * 1000 + 1000 - 1;
            // 推送到第三方审核平台
            // 这个假设有 10000条数据,那摩每次推送处理1000条
            threadService.push(array,s,e);
        }
        
        long end = System.currentTimeMillis();
    }
}

3.启动测试

@Test
     private PushService pushservice;
    void test2(){
       pushService.pushNew();
    }

这里可以发现,虽然上述方法可以达到异步执行的目的,但是我们并不知道线程执行的结果,有没有执行成功,因为这种方式有时候在企业中并不是最佳使用方式,下面介绍带有返回值的多线程

1.还是使用上述的线程池

    
    @Service
public class PushService {
    @Autowired
    private ThreadPoolTaskExecutor asyncServiceExecutor;
    
    /**
    * 线程要执行的任务,注意这里不能使用async了,
    * 因为没有submit方法得到返回值,只能手动通过线程池对象调用该方法
    */
    public Integer push1(int[] array, int start, int end) {
        int count = 0;
        long s = System.currentTimeMillis();
        for (int i = start; i <= end ; i++) {
            pushSend(array[i]);
            count++;
            // 推送失败,可以记录日志
        }

        long e = System.currentTimeMillis();
        System.out.println((e - s)+ "ms");
        return count;
    }     

}    

2.手动通过线程池对象执行任务并获取返回值

/**
     * 使用带有返回值的线程池
     */
    public void pushNewCall(){
        // 假设我们要推送一万条数据
        int dataNum = 10000;
        int[] array = new int[dataNum];
        for (int i = 0; i < dataNum; i++) {
            array[i] = i;
        }

        long start = System.currentTimeMillis();

        /**
         * 推送的数据数量
         * 假设线程池的数量为10,也就是说每个线程处理1000条数据,共需要10次循环
         */
        List<Future> futureList = new ArrayList<>();
        for (int i = 0; i < 10 ; i++) {
            int s = i * 1000;
            int e = i * 1000 + 1000 - 1;
            // 推送到第三方平台
            // 这个是假设,有10000条数据,那摩每次推送处理1000条数据
            Future<Integer> submit = asyncServiceExecutor.submit(new Callable<Integer>() {

                @Override
                public Integer call() throws Exception {
                    return threadService.push1(array, s, e);
                }
            });
            // 注意,在这里不能直接得到返回值,因为submit.get()方法会阻塞获取执行结果
            futureList.add(submit);
        }


        for (Future future : futureList) {
            try {
                System.out.println("本轮线程执行数量:"+future.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        long end = System.currentTimeMillis();
        System.out.println((end - start) + "ms");
    }

3.测试

@Test
    void test3(){
        pushService.pushNewCall();
        try {
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

至此,大功告成,如果线程执行失败,就可以从执行结果来进行相应的补偿措施

你可能感兴趣的:(JUC,java,springboot)