如何自定义线程池的?如何合理设置线程池的参数?


你是如何自定义线程池的?如何合理设置线程池的参数?

使用 ThreadPoolExecutor 实现灵活的自定义线程池,并通过 ArrayBlockingQueue 存放任务。 针对每类不同的业务,分别定义不同的线程池,让它们互不影响。

使用线程池可以避免频繁地创建和销毁线程会带来显著的性能开销,线程池的管理通常依赖于特定的编程框架或库,其中ThreadPoolExecutor是Java中一个非常常用的线程池管理工具

示例代码:

ExecutorService executorService = 
    new ThreadPoolExecutor(40, 1000, 10000, TimeUnit.MINUTES, new ArrayBlockingQueue<>(10000));

自定义线程池参数如下:

  1. 核心线程数(corePoolSize):线程池中一直保持活动的线程数。可以使用corePoolSize方法来设置。一般情况下,可以根据系统的资源情况和任务的特性来设置合适的值。

  2. 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数。可以使用maximumPoolSize方法来设置。如果所有线程都处于活动状态,而此时又有新的任务提交,线程池会创建新的线程,直到达到最大线程数。

  3. 空闲线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,如果这些线程在一定时间内没有执行任务,则这些线程会被销毁。可以使用keepAliveTime和TimeUnit方法来设置。

  4. 阻塞队列(workQueue):用于存放等待执行的任务的阻塞队列。可以根据任务的特性选择不同类型的队列,如LinkedBlockingQueue、ArrayBlockingQueue等。默认情况下,使用无界阻塞队列,即LinkedBlockingQueue,但也可以根据需要设置有界队列。

    • ArrayBlockingQueue 是一个由数组支持的有界阻塞队列。这个队列按 FIFO(先进先出)原则对元素进行排序。ArrayBlockingQueue 需要在创建时指定容量,且一旦创建后不能更改。此队列使用单个锁来控制插入和移除操作,从而导致这两种操作不能完全并行。

    • LinkedBlockingQueue 是一个由链表结构支持的可选有界队列。

      LinkedBlockingQueue 内部使用两个锁,一个用于入队操作,一个用于出队操作,允许这两个操作并行进行,从而提高了队列在并发环境中的吞吐量。

    • PriorityBlockingQueue 是一个支持优先级排序的无界阻塞队列。队列中元素的排序可以根据自然排序,或者根据构造时提供的 Comparator 来进行。

      PriorityBlockingQueue 通常用于执行基于优先级的任务调度。注意此队列不阻塞数据插入操作,但如果队列为空,数据取出操作会阻塞。

    • SynchronousQueue 是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,反之亦然。

      SynchronousQueue 适合于传递性的任务调度,每个任务都由一个线程提交,另一个线程接收执行。

      import java.util.concurrent.*;
      
      public class ThreadPoolWithArrayBlockingQueue {
          public static void main(String[] args) {
              BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
              ThreadPoolExecutor executor = new ThreadPoolExecutor(
                  2, 4, 1, TimeUnit.MINUTES, queue);
      
              for (int i = 0; i < 20; i++) {
                  final int taskId = i;
                  executor.execute(() -> {
                      System.out.println("Executing task " + taskId +
                                         " on thread " + Thread.currentThread().getName());
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          Thread.currentThread().interrupt();
                      }
                  });
              }
      
              executor.shutdown();
          }
      }
      
      

      在这个例子中,我们创建了一个有界队列 ArrayBlockingQueue,用作线程池的工作队列。线程池被配置为有 2 个核心线程,最多 4 个线程,并且有一个容量为 10 的队列用于存储待处理任务。

  5. 线程工厂(threadFactory):用于创建线程的工厂。可以通过实现ThreadFactory接口自定义线程的创建逻辑。

  6. 拒绝策略(rejectedExecutionHandler):当线程池无法接受新的任务时,会根据设置的拒绝策略进行处理。常见的拒绝策略有AbortPolicy、DiscardPolicy、DiscardOldestPolicy和CallerRunsPolicy。

我们是会根据任务的类型以及消耗资源的情况来调整线程池的参数。

  1. 比如针对更消耗 CPU 资源的计算密集型任务,我会将核心线程数设置为和 CPU 核心数相同,充分利用系统资源;
  2. 针对更消耗网络等 IO 的 IO 密集型任务,我会将核心线程数设置得更大,比如 CPU 核心数的 2 - 4 倍,能够增加并发度、并且提高 CPU 的利用率。

有一个经验值公式,其中 N 为 CPU 核心数:CPU 密集型任务,核心线程数设置为 N(或 N + 1);IO 密集型任务,核心线程数设置为 2N。

JAVA-技能点
多线程