工作中遇到的关于多线程的疑问

176 阅读3分钟
  1. 情景描述

    最近在工作中正式遇到了使用多线程的情景,即在主程序运行后会开启一个定时任务,这个定时任务是每隔一分钟就去检查是否有满足发送条件但还未发送的短信。同时发送短信的这个方法是以多线程的形式进行发送的,不影响其它线程的进行。

  2. 代码

    • 定时任务

      @Component
      @Slf4j
      public class SendMmsTask {
          @Autowired
          private MmsTemplateService mmsTemplateService;
      ​
          @Scheduled(cron = "0 0/1 * * * ?")
          public void sendMmsTaskMessage() {
              log.info("============> mms发送定时任务短信开始");
              try {
                  mmsTemplateService.sendMsg();
              } catch (Exception e) {
                  log.error("==={}", e);
              }
              log.info("============> mms发送定时任务短信结束");
          }
      }
      

      注意:定时任务是单线程的,要实现定时任务以多线程执行需要去编写一个关于定时任务的配置文件,在此不展示

    • 消息队列以及部分发送短信方法

      BlockingQueue<PhoneDataResponse> queue = new LinkedBlockingQueue<>(1024 * 1024);
      ​
      //其中sendMsg方法调用了此方法
      public synchronized Map<String, String> realSendMsg(MmsTemplate mmsTemplate){
          ...
              datas.stream().forEach(item->{
              queue.offer(item);
          });
      }
      
    • 初始化方法

      @PostConstruct
      public void init() {
          log.info("我是init方法");
          UpdateUsersPhoneRecord task = new UpdateUsersPhoneRecord();
          Thread thread = new Thread(task);
          thread.setName("mms update user_phone_record");
          thread.start();
      }
      

      注意:我在网上查到的@PostConstruct注解的意思是@PostConstruct注解的方法将会在依赖注入完成后被自动调用

    • 实现Runnable接口的类

      class UpdateUsersPhoneRecord implements Runnable {
          @Override
          public void run() {
              for(;;){
                  try{
                      PhoneDataResponse task = queue.take();
                      if(0 == task.getCode()){
                          mmsUserPhoneRecordService.updateMmsUserPhoneStatus(task.getTaskId(), task.getPhone(), SmsSendStatusEnum.PASS.getCode());
                      }else{
                          log.info("===taskId:{}, phone:{},item.message:{}===", task.getTaskId(), task.getPhone(), task.getMessage());
                          mmsUserPhoneRecordService.updateMmsUserPhoneStatus(task.getTaskId(), task.getPhone(), SmsSendStatusEnum.UN_PASS.getCode());
                      }
                  }catch (InterruptedException e){
                      System.out.println("我在这里");
                      e.printStackTrace();
                  }
              }
          }
      }
      

      注意:updateMmsUserPhoneStatus方法是用来修改短信被发送后的短信状态信息

  3. 问题

    因为之前没有实际编写过定时任务与多线程的需求,所以在项目中第一次碰到后也是看了许久,但还是有些疑问。 (1)首先是被@PostConstruct注解的方法会在依赖注入完成后被自动调用,但是定时任务是每分钟执行一次,我在启动程序并观察消息日志后发现了这个:

image-20220111172730032.png 确实是init方法先执行,然后再过一段时间后执行定时任务,但我也出现了疑问,在init执行后和定时任务执行前的这段时间里,队列queue中是没有存放PhoneDataResponse对象的,但是init方法中执行了thread.start(),也就是说重写的run方法也被调用了,那queue.take()也取不出对象了,但是在这段时间内控制台并没有打印出字符串“我在这里”,这说明这一部分并没有报错,那么为什么不会报错呢?

(2)为什么要使用@PostConstruct注解先去调用init方法,而不是将其放在定时任务中?

  1. 询问组长的结果

    (1)首先是对第一个问题的解答,我去询问了组长,这才发现这是一个关于阻塞的问题,当queue中没有对象而程序需要消费时,队列就会被阻塞而且不会报错,因此上面调用init方法后和执行定时任务这段时间线程实际上是在阻塞状态的,当定时任务开始后queue里就有了需要的对象,此时不为空,队列里有了任务,因此线程从阻塞状态恢复为运行状态。

    (2)第二个问题则是使用场景的问题,首先这个定时任务的主要目的是用来给用户定时发短信,并不是需要很多资源和时间去调用,因此只需要额外开启一个线程去执行就足够了,如果按照我当时的逻辑去编写,那么出现的状况便是每执行一次定时任务就会新建一个线程,从业务需求上和开销上都是没有什么意义的,所以init方法只需要依赖注入完成后被调用一次即可。