如何将线程池运用于具体工作场景?

832 阅读7分钟

如何将线程池运用于具体工作场景?

image.png

线程池简介

  • 什么是线程池?

    • 线程池,就是管理 n 个线程的池子。池子中的线程可以用来处理任务,并且线程池中的线程可以复用。这样相对于每次执行一个任务就需要开启一个线程,节省了很大一部分的 CPU、内存、磁盘 的开销。
  • 为什么要用线程池?

    • 降低资源开销。通过对线程池中的线程进行复用,节省线程的创建、销毁等产生的消耗。
    • 提高任务执行效率。执行任务,直接使用线程池中的线程,减少创建线程的时间。
    • 方便管理线程。对线程进行管理,避免创建过多线程导致内存溢出或者内存泄漏。

线程池有哪些?

  1. 固定线程数的线程池(FixedThreadPool
  • 适用于 负载稳定 的场景,线程数固定表示不会经常创建和销毁线程,执行效率较高。
//线程数固定为10的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
  1. 只有一个线程的线程池(SingleThreadExecutor
  • 适用于 需要顺序执行任务 的场景,所有的任务都在同一个线程中执行。
//线程数只有一个的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
  1. 可调整线程数的线程池(CachedThreadPool
  • 适用于 任务数动态变化 的场景,线程池中的线程数量可根据需要创建新线程,也会复用已经创建的线程。
//线程数可以动态变化的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
  1. 定时或周期性执行的线程池(ScheduledThreadPool
  • 适用于 执行定时或者周期性任务 的场景,可以指定任务在指定的时间之后执行,或者定期执行。
//线程数为10的线程池,可定时或周期执行任务
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);

以下是使用 FixedThreadPool 计算 100 次,1 到 100 的累加和的使用案例,固定 10 个线程执行。

public static final ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
    public static void main(String[] args) {
        System.out.println("开始使用:fixedThreadPool!");
        //使用线程池 fixedThreadPool 计算 100 次,1 到 100 的累加和。
        for (int i = 0; i < 100; i++) {
            fixedThreadPool.execute(()->{
                calculateSum(100);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        }
​
    }
​
    public static void calculateSum(int n){
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        System.out.println("CurrentThreadName:" + Thread.currentThread().getName() + "。Sum of 1 to 100: " + sum);
    }

但是,不推荐使用这四种方法去创建线程池,建议使用 ThreadPoolExecutor 构造函数的方式创建线程池,原因如下:

  • FixedThreadPoolSingleThreadExecutor 等待队列为:LinkedBlockingQueue,最大长度等于 Integer.MAX_VALUE,如果大量任务堆积,会造成 OOM。
  • CachedThreadPool 等待队列为:SynchronousQueue,并且允许创建线程数等于 Integer.MAX_VALUE, 如果等待任务很多,不断创建线程,最终会导致 OOM。
  • ScheduledThreadPool 等待队列为:DelayedWorkQueue,最大长度为 Integer.MAX_VALUE,如果大量任务堆积,会造成 OOM。

使用 ThreadPoolExecutor 自定义线程池

JDK 源码中,详细描述了如何使用 ThreadPoolExecutor 自定义线程池。如下图:

image.png

  • corePoolSize:核心线程数,即使线程是空闲的,线程池也会保持存活的线程数,除非调用了 allowCoreThreadTimeOut 方法,并设置为 true,相当于设置核心线程的存活时间为:keepAliveTime
  • maximumPoolSize:线程池中允许的最大线程数。
  • keepAliveTime:当线程数超过核心线程数时,多余的空闲线程的存活时间。
  • unitkeepAliveTime 的时间单位。
  • workQueue:工作队列,用于存放待执行的任务。
  • threadFactory:线程工厂,用于创建新的线程。
  • handler:拒绝策略,当线程池和工作队列都满了,如何处理新加入的任务。

线程池拒绝策略:

  1. AbortPolicy:抛出 RejectedExecutionException 异常,拒绝新来的任务。
  2. CallerRunsPolicy:使用调用线程执行线程池中的线程,主线程。
  3. DiscardPolicy:不处理,直接丢弃。
  4. DiscardOldestPolicy:丢弃等待队列中的最老任务。

以下是使用 ThreadPoolExecutor 计算 100 次,1 到 100 的累加和的使用案例

  • corePoolSize(核心线程):10。
  • maximumPoolSize(最大线程):20。
  • keepAliveTime(存活时间):300。
  • unit(存活时间单位):秒。
  • workQueue(工作队列):60。
  • handler(拒绝策略):CallerRunsPolicy
public static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 300, TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(60), new ThreadPoolExecutor.CallerRunsPolicy());
    public static void main(String[] args) {
        System.out.println("开始使用:ThreadPoolExecutor!");
        //使用线程池 ThreadPoolExecutor 计算 100 次,1 到 100 的累加和。
        for (int i = 0; i < 100; i++) {
            threadPoolExecutor.execute(()->{
                calculateSum(100);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        }
​
    }
    /**
     * 计算 1 到 n 的累加和
     */
    public static void calculateSum(int n){
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        System.out.println("CurrentThreadName:" + Thread.currentThread().getName() + "。Sum of 1 to 100: " + sum);
    }

推荐,最大线程数为核心线程数的 2 倍,等待队列是最大线程数的 2-3 倍。

线程池如何执行

此处引用美团技术博客中,线程池的执行流程图

Java线程池实现原理及其在美团业务中的实践

image.png

  1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
  2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
  3. 如果任务队列已经满了,但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
  4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用 RejectedExecutionHandler.rejectedExecution() 方法,也就是线程池拒绝策略

线程池使用场景

  • CPU 密集型场景:把核心线程数设置为 CPU 核心 + 1。(《Java 并发编程实战》给出的原因是:即使当计算(CPU)密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保 CPU 的时钟周期不会被浪费。 )但是也要考虑到,如果服务器中部署多个应用,得考虑其他应用的线程池使用情况。
  • IO 密集型场景
  • 《Java 并发编程实战》书中给出的计算方式,如下图:

image.png
核心线程数 = CPU核数 * CPU 目标使用率 * (1 + 线程等待耗时 / 线程计算耗时)

  • 但是,线程等待耗时和线程计算耗时,在实际开发中难以确定。而且 Java 线程池可以利用线程切换的方式,最大程度利用 CPU 核数,这样计算出来的核心线程数,十分偏离实际业务场景。为了解决这些问题,我们可以基于不同时间的业务流量,周期性/定时动态修改线程池的参数

动态修改线程池参数

  • 此处引用美团技术博客中,设置核心线程数setCorePoolSize)流程图。

Java线程池实现原理及其在美团业务中的实践

image.png

  1. 在运行期线程池使用方调用此方法设置 corePoolSize 之后,线程池会直接覆盖原来的 corePoolSize 值,并且基于当前值和原始值的比较结果采取不同的处理策略。
  2. 对于当前值小于当前工作线程数的情况,说明有多余的 worker 线程,此时会向当前 idle (空闲)的 worker 线程发起中断请求以实现回收,多余的 worker 在下次 idel 的时候也会被回收;
  3. 对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的 worker 线程来执行队列任务。
  • 注意:如果修改了核心线程数,但是没有对应修改最大线程数,可能造成最终的活跃线程数还是之前的最大线程数。

动态修改核心线程数、最大线程数、存活时间大小、线程工厂、拒绝策略,如下所示。

public static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 300, TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(60), new ThreadPoolExecutor.CallerRunsPolicy());
    public static void main(String[] args) {
        System.out.println("开始使用:fixedThreadPool!");
        //使用线程池 fixedThreadPool 计算 100 次,1 到 100 的累加和。
        for (int i = 0; i < 100; i++) {
            threadPoolExecutor.execute(()->{
                calculateSum(100);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            threadPoolExecutor.setCorePoolSize(20);
            threadPoolExecutor.setMaximumPoolSize(30);
            threadPoolExecutor.setKeepAliveTime(600, TimeUnit.SECONDS);
            threadPoolExecutor.setThreadFactory(new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread.currentThread().setName("test");
                    return Thread.currentThread();
                }
            });
            threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
        }
    }

由于 LinkedBlockingDeque 的容量是 final 常量,如下图。所以,如果想要动态修改工作队列大小,需要自己创建一个队列,设置容量private int capacity

image.png

以上就是全部内容了,包含了什么是线程池、为什么要用线程池、线程池有哪些、线程池如何执行、线程池的使用场景、如何修改线程池参数