持续创作,加速成长!这是我参加「日新计划 10 月更文挑战」的第8天,点击查看活动概况

作为一个Java开发线程池的运用是一个绕不过去的坎,如何正确的运用线程池是每个开发面对的问题,今日咱们就从开源项目中来看看那些顶级开源项目中是如何运用线程池。下面咱们就以笔者最近参加的开源项目RocketMQ为比如同时结合笔者在工作中遇到的一些运用一些不好的习惯来说一下线程池运用。会从一下几个方面来说:

从开源项目看线程池的使用

1. 线程池的创立

首先咱们看一下RocketMQ的线程创立方式,以 BrokerController#initializeResources 办法为比如。如下图:

从开源项目看线程池的使用

初看一下好像是自定义的线程池,查看源码就会发现 BrokerFixedThreadPoolExecutor 其实是实现了 ThreadPoolExecutor 本质上还是 ThreadPoolExecutor ,仅仅进行了语义化。剖析上面的线程池创立会发现有以下几个特点:

  • 线程池都会设置称号(推荐设置)

    为什么需要设置称号?最主要的原因便是在排查问题的时分可以知道是哪个线程池履行的什么代码片段出了问题。如果设置称号运用ThreadPoolExecutor默许的称号会在排查问题的时分不知道是哪个线程池。

  • 设置线程池线程数量

    这里会依据中心线程数以及最大线程数来判断这个线程池的线程是否需要扩容,例如中心线程数10,最大线程池数100.那么线程池便是在运转过程中超过了中心线程数后就会持续创立线程来满意使命履行。看RocketMQ中的中心线程池和最大线程池一般设置成一样大,也便是固定巨细的线程池。

  • 自定义使命行列的设置容量(推荐设置)

    在RocketMQ中许多都是运用的 LinkedBlockingQueue ,然后给行列设置容量。要点:行列挑选依据个人的需求进行挑选,可是必定要给行列设置一个合理的容量。如果不设置容量那么默许的容量便是 Integer.MAX_VALUE ,这可能很大程度上会导致在触发使命回绝战略之前内存已经耗光了导致服务宕机。

  • 使命回绝战略挑选

    使命回绝战略挑选一般情况下可以运用JDK ThreadPoolExecutor的默许战略,如果有特别的需求用户可以自定义战略。

上面是从RocketMQ的开源项目看到的线程池的创立。其实许多人会发现很少用 Executors 创立。想要具体了解可以看一下 《为什么不建议运用Executors创立线程池剖析》 这篇文章。笔者在这篇文章给出了剖析。

2. 线程池的运用

研讨过RocketMQ源码的人就会发现,RocketMQ有许许多多的线程池,许多人肯定会想问为什么不必一个线程池完成一切的使命。笔者使命主要有以下几个原因。

2.1 单一性准则

单一性准则怎么理解?便是一个线程池应该是做一类使命处理。从 BrokerController#initializeResources 中的代码可以看出来不同的使命运用不同的线程池来处理。这样做的好处:

  • 便利回溯线程履行问题,在线程池履行产生过错的时分,这个时分就需要依据过错堆栈进行追寻单一性就便利回溯。如果一切的使命都运用一个线程池去履行,经过线程池称号你是不知道是哪个使命有问题。
  • 可以更好的评价行列的巨细
  • 线程池履行过程中出问题导致线程池shutdown或者其他问题只会影响到一类使命不会影响到全部使命

2.2 适当的封装

对线程池进行适当的封装可以在满意安全性的同时还能让代码看起来愈加的优雅,也便是上文说到的语义化

3. 线程池过错运用

在笔者之前的文章中 《Java线程池运用不当会产生什么-出产事例》 给过一个公司出产事例,便是在Controller调用的办法中创立线程池然后去履行异步使命。在这种情况下会出现什么情况便是每次都会创立一个线程池然后这个线程池还不会被销毁,所以每调用一次办法就会创立一个至少一个线程。如果调用1000次办法就会导致有1000个线程被创立。

@Service
@Slf4j
public class AccountAuthServiceImpl implements AccountAuthService {
   //省掉了部分代码
  
   //功能:将两个体系账号进行绑定
   @Override
   public boolean bindUser(Long hrsAccountId, Long hmcAccountId){
    ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*2);
    Future<Boolean> hrsExt = executorService.submit(new Callable<Boolean>() {
      @Override
      public Boolean call() throws Exception {
        //查询hrsAccountId是否存在
        return true;
       }
     });
    Future<Boolean> hmcExt = executorService.submit(new Callable<Boolean>() {
      @Override
      public Boolean call() throws Exception {
        //查询hmcAccountId是否存在
        return true;
       }
     });
    try {
      return hrsExt.get(3, TimeUnit.SECONDS) & hmcExt.get(3, TimeUnit.SECONDS);
     } catch (InterruptedException e) {
      e.printStackTrace();
     } catch (ExecutionException e) {
      e.printStackTrace();
     } catch (TimeoutException e) {
      e.printStackTrace();
     }
    return false;
   }
}

4. 总结

  • 线程池和线程必定要设置开发者了解的称号,便利出现问题时分的排查,这个在其他项目和Java中经过指令都可以发现,线程命名是很重要的一个步骤。虽然Java供给了默许的姓名,可是在排查问题中自定义称号显得尤为重要
  • 线程池的使命行列容量必定要设置,不然可能会导致耗光内存导致服务宕机
  • 线程池的创立不能放在事务办法中创立

我是蚂蚁背大象(GitHub:mxsm),文章对你有帮助点赞重视我,文章有不正确的地方请您指正留言评论~谢谢!