线程池最佳实践!安排!

你们好,我是 Guide 哥,一个三观比主角还正的技术人。今天再来继续聊聊线程池~html

线程池最佳实践

这篇文章篇幅虽短,可是绝对是干货。标题稍微有点夸张,嘿嘿,实际都是本身使用线程池的时候总结的一些我的感受比较重要的点。java

线程池知识回顾

开始这篇文章以前仍是简单介绍一嘴线程池,以前写的《新手也能看懂的线程池学习总结》这篇文章介绍的很详细了。git

为何要使用线程池?

池化技术相比你们已经家常便饭了,线程池、数据库链接池、Http 链接池等等都是对这个思想的应用。池化技术的思想主要是为了减小每次获取资源的消耗,提升对资源的利用率。github

线程池提供了一种限制和管理资源(包括执行一个任务)。 每一个线程池还维护一些基本统计信息,例如已完成任务的数量。web

这里借用《Java 并发编程的艺术》提到的来讲一下使用线程池的好处面试

  • 下降资源消耗。经过重复利用已建立的线程下降线程建立和销毁形成的消耗。
  • 提升响应速度。当任务到达时,任务能够不须要的等到线程建立就能当即执行。
  • 提升线程的可管理性。线程是稀缺资源,若是无限制的建立,不只会消耗系统资源,还会下降系统的稳定性,使用线程池能够进行统一的分配,调优和监控。

线程池在实际项目的使用场景

线程池通常用于执行多个不相关联的耗时任务,没有多线程的状况下,任务顺序执行,使用了线程池的话可以让多个不相关联的任务同时执行。数据库

假设咱们要执行三个不相关的耗时任务,Guide 画图给你们展现了使用线程池先后的区别。编程

注意:下面三个任务可能作的是同一件事情,也多是不同的事情。微信

使用线程池先后对比
使用线程池先后对比

如何使用线程池?

通常是经过 ThreadPoolExecutor 的构造函数来建立线程池,而后提交任务给线程池执行就能够了。网络

ThreadPoolExecutor构造函数以下:

/**  * 用给定的初始参数建立一个新的ThreadPoolExecutor。  */  public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量  int maximumPoolSize,//线程池的最大线程数  long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间  TimeUnit unit,//时间单位  BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列  ThreadFactory threadFactory,//线程工厂,用来建立线程,通常默认便可  RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,咱们能够定制策略来处理任务  ) {  if (corePoolSize < 0 ||  maximumPoolSize <= 0 ||  maximumPoolSize < corePoolSize ||  keepAliveTime < 0)  throw new IllegalArgumentException();  if (workQueue == null || threadFactory == null || handler == null)  throw new NullPointerException();  this.corePoolSize = corePoolSize;  this.maximumPoolSize = maximumPoolSize;  this.workQueue = workQueue;  this.keepAliveTime = unit.toNanos(keepAliveTime);  this.threadFactory = threadFactory;  this.handler = handler;  } 复制代码

简单演示一下如何使用线程池,更详细的介绍,请看:《新手也能看懂的线程池学习总结》

private static final int CORE_POOL_SIZE = 5;
 private static final int MAX_POOL_SIZE = 10;  private static final int QUEUE_CAPACITY = 100;  private static final Long KEEP_ALIVE_TIME = 1L;   public static void main(String[] args) {   //使用阿里巴巴推荐的建立线程池的方式  //经过ThreadPoolExecutor构造函数自定义参数建立  ThreadPoolExecutor executor = new ThreadPoolExecutor(  CORE_POOL_SIZE,  MAX_POOL_SIZE,  KEEP_ALIVE_TIME,  TimeUnit.SECONDS,  new ArrayBlockingQueue<>(QUEUE_CAPACITY),  new ThreadPoolExecutor.CallerRunsPolicy());   for (int i = 0; i < 10; i++) {  executor.execute(() -> {  try {  Thread.sleep(2000);  } catch (InterruptedException e) {  e.printStackTrace();  }  System.out.println("CurrentThread name:" + Thread.currentThread().getName() + "date:" + Instant.now());  });  }  //终止线程池  executor.shutdown();  try {  executor.awaitTermination(5, TimeUnit.SECONDS);  } catch (InterruptedException e) {  e.printStackTrace();  }  System.out.println("Finished all threads");  } 复制代码

控制台输出:

CurrentThread name:pool-1-thread-5date:2020-06-06T11:45:31.639Z
CurrentThread name:pool-1-thread-3date:2020-06-06T11:45:31.639Z CurrentThread name:pool-1-thread-1date:2020-06-06T11:45:31.636Z CurrentThread name:pool-1-thread-4date:2020-06-06T11:45:31.639Z CurrentThread name:pool-1-thread-2date:2020-06-06T11:45:31.639Z CurrentThread name:pool-1-thread-2date:2020-06-06T11:45:33.656Z CurrentThread name:pool-1-thread-4date:2020-06-06T11:45:33.656Z CurrentThread name:pool-1-thread-1date:2020-06-06T11:45:33.656Z CurrentThread name:pool-1-thread-3date:2020-06-06T11:45:33.656Z CurrentThread name:pool-1-thread-5date:2020-06-06T11:45:33.656Z Finished all threads 复制代码

线程池最佳实践

简单总结一下我了解的使用线程池的时候应该注意的东西,网上彷佛尚未专门写这方面的文章。

由于 Guide 还比较菜,有补充和完善的地方,能够在评论区告知或者在微信上与我交流。

1. 使用 ThreadPoolExecutor 的构造函数声明线程池

1. 线程池必须手动经过 ThreadPoolExecutor 的构造函数来声明,避免使用Executors 类的 newFixedThreadPoolnewCachedThreadPool ,由于可能会有 OOM 的风险。

Executors 返回线程池对象的弊端以下:

  • FixedThreadPoolSingleThreadExecutor : 容许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而致使 OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 容许建立的线程数量为 Integer.MAX_VALUE ,可能会建立大量线程,从而致使 OOM。

说白了就是:使用有界队列,控制线程建立数量。

除了避免 OOM 的缘由以外,不推荐使用 Executors提供的两种快捷的线程池的缘由还有:

  1. 实际使用中须要根据本身机器的性能、业务场景来手动配置线程池的参数好比核心线程数、使用的任务队列、饱和策略等等。
  2. 咱们应该显示地给咱们的线程池命名,这样有助于咱们定位问题。

2.监测线程池运行状态

你能够经过一些手段来检测线程池的运行状态好比 SpringBoot 中的 Actuator 组件。

除此以外,咱们还能够利用 ThreadPoolExecutor 的相关 API 作一个简陋的监控。从下图能够看出, ThreadPoolExecutor提供了线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。

下面是一个简单的 Demo。printThreadPoolStatus()会每隔一秒打印出线程池的线程数、活跃线程数、完成的任务数、以及队列中的任务数。

/**  * 打印线程池的状态  *  * @param threadPool 线程池对象  */  public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {  ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-thread-pool-status", false));  scheduledExecutorService.scheduleAtFixedRate(() -> {  log.info("=========================");  log.info("ThreadPool Size: [{}]", threadPool.getPoolSize());  log.info("Active Threads: {}", threadPool.getActiveCount());  log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount());  log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());  log.info("=========================");  }, 0, 1, TimeUnit.SECONDS);  } 复制代码

3.建议不一样类别的业务用不一样的线程池

不少人在实际项目中都会有相似这样的问题:个人项目中多个业务须要用到线程池,是为每一个线程池都定义一个仍是说定义一个公共的线程池呢?

通常建议是不一样的业务使用不一样的线程池,配置线程池的时候根据当前业务的状况对当前线程池进行配置,由于不一样的业务的并发以及对资源的使用状况都不一样,重心优化系统性能瓶颈相关的业务。

咱们再来看一个真实的事故案例! (本案例来源自:《线程池运用不当的一次线上事故》 ,很精彩的一个案例)

案例代码概览
案例代码概览

上面的代码可能会存在死锁的状况,为何呢?画个图给你们捋一捋。

试想这样一种极端状况:

假如咱们线程池的核心线程数为 n,父任务(扣费任务)数量为 n,父任务下面有两个子任务(扣费任务下的子任务),其中一个已经执行完成,另一个被放在了任务队列中。因为父任务把线程池核心线程资源用完,因此子任务由于没法获取到线程资源没法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程池资源,这也就形成了 "死锁"

解决方法也很简单,就是新增长一个用于执行子任务的线程池专门为其服务。

4.别忘记给线程池命名

初始化线程池的时候须要显示命名(设置线程池名称前缀),有利于定位问题。

默认状况下建立的线程名字相似 pool-1-thread-n 这样的,没有业务含义,不利于咱们定位问题。

给线程池里的线程命名一般有下面两种方式:

**1.利用 guava 的 ThreadFactoryBuilder **

ThreadFactory threadFactory = new ThreadFactoryBuilder()
 .setNameFormat(threadNamePrefix + "-%d")  .setDaemon(true).build(); ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory) 复制代码

2.本身实现 ThreadFactor

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; /**  * 线程工厂,它设置线程名称,有利于咱们定位问题。  */ public final class NamingThreadFactory implements ThreadFactory {   private final AtomicInteger threadNum = new AtomicInteger();  private final ThreadFactory delegate;  private final String name;   /**  * 建立一个带名字的线程池生产工厂  */  public NamingThreadFactory(ThreadFactory delegate, String name) {  this.delegate = delegate;  this.name = name; // TODO consider uniquifying this  }   @Override  public Thread newThread(Runnable r) {  Thread t = delegate.newThread(r);  t.setName(name + " [#" + threadNum.incrementAndGet() + "]");  return t;  }  } 复制代码

5.正确配置线程池参数

说到如何给线程池配置参数,美团的骚操做至今让我难忘(后面会提到)!

咱们先来看一下各类书籍和博客上通常推荐的配置线程池参数的方式,能够做为参考!

常规操做

不少人甚至可能都会以为把线程池配置过大一点比较好!我以为这明显是有问题的。就拿咱们生活中很是常见的一例子来讲:并非人多就能把事情作好,增长了沟通交流成本。你原本一件事情只须要 3 我的作,你硬是拉来了 6 我的,会提高作事效率嘛?我想并不会。 线程数量过多的影响也是和咱们分配多少人作事情同样,对于多线程这个场景来讲主要是增长了上下文切换成本。不清楚什么是上下文切换的话,能够看我下面的介绍。

上下文切换:

多线程编程中通常线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能获得有效执行,CPU 采起的策略是为每一个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会从新处于就绪状态让给其余线程使用,这个过程就属于一次上下文切换。归纳来讲就是:当前任务在执行完 CPU 时间片切换到另外一个任务以前会先保存本身的状态,以便下次再切换回这个任务时,能够再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

上下文切换一般是计算密集型的。也就是说,它须要至关可观的处理器时间,在每秒几十上百次的切换中,每次切换都须要纳秒量级的时间。因此,上下文切换对系统来讲意味着消耗大量的 CPU 时间,事实上,多是操做系统中时间消耗最大的操做。

Linux 相比与其余操做系统(包括其余类 Unix 系统)有不少的优势,其中有一项就是,其上下文切换和模式切换的时间消耗很是少。

类比于实现世界中的人类经过合做作某件事情,咱们能够确定的一点是线程池大小设置过大或者太小都会有问题,合适的才是最好。

若是咱们设置的线程池数量过小的话,若是同一时间有大量任务/请求须要处理,可能会致使大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了以后任务/请求没法处理的状况,或者大量任务堆积在任务队列致使 OOM。这样很明显是有问题的! CPU 根本没有获得充分利用。

可是,若是咱们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会致使大量的上下文切换,从而增长线程的执行时间,影响了总体执行效率。

有一个简单而且适用面比较广的公式:

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,能够将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它缘由致使的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种状况下多出来的一个线程就能够充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就能够将 CPU 交出给其它线程使用。所以在 I/O 密集型任务的应用中,咱们能够多配置一些线程,具体的计算方法是 2N。

如何判断是 CPU 密集任务仍是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务好比你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特色是 CPU 计算耗费时间相比于等待 IO 操做完成的时间来讲不多,大部分时间都花在了等待 IO 操做完成上。

美团的骚操做

美团技术团队在《Java 线程池实现原理及其在美团业务中的实践》这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。

美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是:

  • corePoolSize : 核心线程数线程数定义了最小能够同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前能够同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,若是达到的话,信任就会被存放在队列中。

为何是这三个参数?

我在这篇《新手也能看懂的线程池学习总结》 中就说过这三个参数是 ThreadPoolExecutor 最重要的参数,它们基本决定了线程池对于任务的处理策略。

如何支持参数动态配置? 且看 ThreadPoolExecutor 提供的下面这些方法。

格外须要注意的是corePoolSize, 程序运行期间的时候,咱们调用 setCorePoolSize()这个方法的话,线程池会首先判断当前工做线程数是否大于corePoolSize,若是大于的话就会回收工做线程。

另外,你也看到了上面并无动态指定队列长度的方法,美团的方式是自定义了一个叫作 ResizableCapacityLinkedBlockIngQueue 的队列(主要就是把LinkedBlockingQueue的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。

最终实现的可动态修改线程池参数效果以下。👏👏👏

动态配置线程池参数最终效果
动态配置线程池参数最终效果

还没看够?推荐 why 神的《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》这篇文章,深度剖析,很不错哦!

做者介绍: Github 80k Star 项目 JavaGuide(公众号同名) 做者。每周都会在公众号更新一些本身原创干货。公众号后台回复“1”领取Java工程师必备学习资料+面试突击pdf。

本文使用 mdnice 排版

相关文章
相关标签/搜索