MENU
// //

探索线程池的威力:优化多线程任务管理与性能提升

September 12, 2023 • 技术分享

比喻举例(可以比作工人队伍)

想象一下,如果我们需要完成很多工作,我们可以招募一群工人来协助。然而,如果每个工人都是临时招募的,工作完成后就解雇他们,那么每次都要花时间和精力来招募和解雇工人,效率会很低。

线程池就像是一个团队,其中包含一些固定数量的常驻工人。这些工人一直在那里,准备接受任务。当有新任务到来时,只需要将任务交给线程池,线程池会选择一个可用的工人来执行任务。

这样做的好处是,线程池中的工人可以被重复利用,无需频繁创建和销毁线程。线程创建和销毁是一个开销较大的操作,通过线程池可以减少这部分开销,提高整体的性能和效率。

线程池还可以控制并发的数量。通过设定线程池的大小,我们可以限制同一时间执行的任务数量,避免资源过度消耗和系统负载过重。线程池可以合理地管理和调度工作,保持系统在高并发情况下的稳定运行。

线程池优点

  1. 降低线程创建和销毁的开销:线程的创建和销毁是一个代价较高的操作。通过使用线程池,我们可以避免频繁地创建和销毁线程,从而减少了这些开销。
  2. 提高系统响应速度:线程池中的线程一直保持活跃状态,可以立即响应新任务的到来。与临时创建的线程相比,线程池中的线程不需要等待线程创建的时间,减少了任务执行的延迟,提高了系统的响应速度。
  3. 控制并发线程数量:线程池可以设定最大并发线程数,以限制同时执行的任务数量。这样可以避免系统过载和资源耗尽,并且可以根据系统的性能和负载情况来灵活调整线程池的大小。
  4. 提供线程管理和调度:线程池通过内部的任务队列来管理待执行的任务。当有新任务提交时,线程池会选择一个可用的线程来执行任务。在任务执行完成后,线程又可以重新回到池中,等待下一个任务的到来。这种自动的管理和调度机制使得线程池更加高效和易于使用。

执行原理

首先讲一下ThreadPoolExecutor中的参数含义

    // Public constructors and methods

    /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters and default thread factory and rejected execution handler.
     * It may be more convenient to use one of the {@link Executors} factory
     * methods instead of this general purpose constructor.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
名称类型含义🔟
corePoolSizeint线程池中保持活跃的线程数,即使它们是空闲状态,除非设置了 allowCoreThreadTimeOut
maximumPoolSizeint线程池允许存在的最大线程数。
keepAliveTimelong当线程数超过 corePoolSize 时,多余的空闲线程在终止之前等待新任务的最大时间
unitTimeUnit参数的时间单位。
workQueueBlockingQueue任务队列,用于存储尚未执行的任务,这个队列只会存储通过 execute 方法提交的 Runnable 任务。
defaultThreadFactoryThreadFactory线程工厂,用于创建新线程
handlerRejectedExecutionHandler拒绝策略,用于在任务被拒绝执行时的处理方式。

看图解释

图片来自 一个会写诗的程序员

img

在了解了这个的前提下我们就可以看下四种类型

线程池类型

固定大小线程池(FixedThreadPool)

固定大小线程池适用于需要固定数量线程的场景。我们可以通过调用Executors.newFixedThreadPool(int nThreads)来创建固定大小线程池,其中nThreads是线程池中的线程数量。

固定大小线程池的核心线程数和最大线程数都是相同的,因此线程数量是固定的。如果所有的工作线程都忙于执行任务,新的任务就会被放入任务队列中等待执行。由于线程数量固定,所以固定大小线程池不会创建新的线程,直到任务队列满了。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomFixedThreadPoolExample {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        int corePoolSize = 5; // 核心线程数
        int maxPoolSize = 5; // 最大线程数
        long keepAliveTime = 0; // 空闲线程的存活时间(单位:秒)

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()
        );

        // 提交任务给线程池执行
        executor.execute(new MyTask());

        // 关闭线程池
        executor.shutdown();
    }

    static class MyTask implements Runnable {
        @Override
        public void run() {
            System.out.println("任务正在执行");
        }
    }
}

缓存线程池(CachedThreadPool)

缓存线程池适用于大量短时间任务的场景。我们可以通过调用Executors.newCachedThreadPool()来创建缓存线程池。

缓存线程池的核心线程数为0,最大线程数为整数最大值。当有任务到来时,如果有空闲线程可用,则直接使用。如果没有空闲线程可用,就会创建新的线程。新线程将在60秒钟不活动后被回收。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        // 创建线程池
        int corePoolSize = 0; // 核心线程数
        int maxPoolSize = Integer.MAX_VALUE; // 最大线程数
        long keepAliveTime = 60; // 空闲线程的存活时间(单位:秒)

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            corePoolSize,
            maxPoolSize,
            keepAliveTime,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()
        );

        // 创建多个任务
        Runnable task1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1正在执行");
            }
        };

        Runnable task2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2正在执行");
            }
        };

        Runnable task3 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务3正在执行");
            }
        };

        // 提交任务到线程池
        executor.execute(task1);
        executor.execute(task2);
        executor.execute(task3);

        // 关闭线程池
        executor.shutdown();
    }
}

//如果想要实现有序的情况下 可以考虑使用executor.submit的方式
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        // 创建线程池
        int corePoolSize = 2; // 核心线程数
        int maxPoolSize = 5; // 最大线程数
        long keepAliveTime = 60; // 空闲线程的存活时间(单位:秒)

        ExecutorService executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()
        );

        // 提交任务
        executor.execute(() -> {
            System.out.println("任务1正在执行");
        });

        executor.execute(() -> {
            System.out.println("任务2正在执行");
        });

        // 关闭线程池
        executor.shutdown();
    }
}

单线程池(SingleThreadExecutor)

单线程池适用于需要按照顺序执行任务的场景。我们可以通过调用Executors.newSingleThreadExecutor()来创建单线程池。

单线程池只有一个工作线程,它会按照任务的提交顺序依次执行任务。如果当前工作线程因为异常退出,线程池会创建一个新的线程来替代它。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomSingleThreadExecutorExample {
    public static void main(String[] args) {
        // 创建单线程池
        int corePoolSize = 1; // 核心线程数
        int maxPoolSize = 1; // 最大线程数
        long keepAliveTime = 0; // 空闲线程的存活时间(单位:秒)

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()
        );

        // 提交任务给线程池执行
        executor.execute(new MyTask());

        // 关闭线程池
        executor.shutdown();
    }

    static class MyTask implements Runnable {
        @Override
        public void run() {
            System.out.println("任务正在执行");
        }
    }
}

定时线程池(ScheduledThreadPool)

定时线程池用于延迟执行或定时执行任务的场景。我们可以通过调用Executors.newScheduledThreadPool(int corePoolSize)来创建定时线程池。

定时线程池可以按照给定的延迟时间或固定的时间间隔周期执行任务。它使用了内部的ScheduledExecutorService来执行定时任务。

//对于延迟执行任务的实例
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ScheduledTaskExample {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);

        // 创建任务1
        Runnable task1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1正在执行");
            }
        };

        // 创建任务2
        Runnable task2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2正在执行");
            }
        };

        // 安排任务1在延迟3秒后执行
        long delay1 = 3;
        executor.schedule(task1, delay1, TimeUnit.SECONDS);

        // 安排任务2在延迟5秒后执行
        long delay2 = 5;
        executor.schedule(task2, delay2, TimeUnit.SECONDS);

        // 关闭线程池
        executor.shutdown();
    }
}

//固定时间间隔
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ScheduledTaskExample {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);

        // 创建任务
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务正在执行");
            }
        };

        // 安排任务每隔1秒执行一次
        long initialDelay = 0;
        long period = 1;
        executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS);

        // 关闭线程池
        executor.shutdown();
    }
}

注意点

为什么不用Executors的方式去建立线程池

  1. 默认参数可能不适合特定的需求:Executors 提供的工厂方法通常使用一组默认的线程池参数,如核心线程数、最大线程数和队列类型等。这些参数对于某些特定的应用场景可能不够合适。如果没有显式地配置这些参数,可能会导致线程池在面对特定负载时表现不佳。
  2. 随意创建线程可能导致资源过度占用:使用 Executors 创建可缓存的线程池时,线程池的最大线程数可以根据需要无限地增长,这意味着可以创建大量的线程。如果不加限制地创建线程,可能会导致系统资源被过度占用,导致性能下降或资源耗尽的风险。
  3. 默认拒绝策略可能不符合特定的需求:当线程池中的线程数达到最大值并且任务队列已满时,Executors 提供了一些默认的拒绝策略,例如抛出异常或丢弃任务。然而,这些默认策略可能不适合特定的应用场景,可能需要自定义拒绝策略来处理满载时的任务。
  4. 隐式创建的线程池可能难以管理和监控:使用 Executors 创建的线程池是隐式的,没有提供直接访问底层线程池实例的方法。这可能导致在需要对线程池进行精确管理、监控和调优时存在困难。

因此,在需要更精确的线程池配置和控制时,手动创建 ThreadPoolExecutor 可以提供更好的灵活性和控制。这样可以更好地满足特定需求,避免不合理的资源占用,并实现更好的性能和可靠性。

优缺点(线程池)

优点缺点
优化资源管理:线程池可以优化线程的创建与销毁,避免频繁创建和销毁线程带来的开销。资源占用:线程池会占用一定的系统资源,包括内存和CPU资源。
提高执行效率:线程池可以重用线程并行执行多个任务,从而提高执行效率。复杂性:线程池的实现较复杂,需要考虑线程池的大小、任务排队策略等。
控制并发数量:线程池可以限制同时执行的线程数量,从而避免资源过度使用和系统负载过高。任务阻塞:线程池在任务队列满时可能会导致新提交的任务等待执行,造成一定的延迟。
统一管理和监控:线程池提供统一的接口来管理和监控线程的执行情况,方便调优和排查问题。适用条件:线程池适用于需要频繁执行、数量较多的任务场景。对于任务较少或执行时间较长的场景,使用线程池可能没有明显的优势。

面试题

什么是线程池?为什么要使用线程池?

  • 线程池是一种用于管理和复用线程的机制。它通过预先创建一组线程,将任务交给这些线程来执行,从而避免了频繁创建和销毁线程的开销。线程池可以提高系统的性能和资源利用率。

Java中常见的线程池有哪些?它们之间有什么区别?

  • Java中常见的线程池有 FixedThreadPoolCachedThreadPoolSingleThreadExecutorScheduledThreadPool
  • FixedThreadPool 在初始化时创建固定数量的线程,适用于长期执行的任务。
  • CachedThreadPool 根据任务的需求动态地调整线程数量,适用于执行大量耗时较短的任务。
  • SingleThreadExecutor 只有一个线程的线程池,适用于需要保证任务按顺序执行的场景。
  • ScheduledThreadPool 可以延迟或定时执行任务的线程池。

线程池中的核心线程数、最大线程数和任务队列有什么作用?

  • 核心线程数是线程池保持的最小线程数量,即使线程是空闲的也会保持存活。
  • 最大线程数是线程池允许的最大线程数量。当线程池已经存在核心线程数,并且任务队列已满时,会创建新的线程直到达到最大线程数。
  • 任务队列用于存储尚未被执行的任务。线程池在执行任务时会从任务队列中取出任务进行处理。

线程池如何处理任务队列已满时的情况?

  • 当任务队列已满且核心线程数达到最大时,线程池会根据指定的拒绝策略来处理新提交的任务。

线程池的拒绝策略有哪些?请分别解释它们的作用。

  • 线程池的拒绝策略有:AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy
  • AbortPolicy 是默认的拒绝策略,它在队列已满的情况下直接抛出 RejectedExecutionException
  • CallerRunsPolicy 将任务交给提交任务的线程去执行,这样可以降低提交任务的速度。
  • DiscardPolicy 直接丢弃无法处理的任务。
  • DiscardOldestPolicy 丢弃队列中最早的未处理任务,尝试重新提交当前任务。

什么是线程池的预启动?如何实现线程池的预启动?

  • 线程池的预启动是指在线程池初始化后,提前创建并启动一定数量的核心线程,使它们处于等待任务的状态。
  • 在 Java 中,通过在创建线程池时使用 prestartCoreThread()prestartAllCoreThreads() 方法来实现线程池的预启动。

线程池中的空闲线程如何管理?它们的存活时间是如何控制的?

  • 线程池中的空闲线程由线程池的控制机制来管理。它们会根据设定的存活时间,在任务执行完毕后保持存活并等待新任务的到来。
  • 存活时间由 keepAliveTime 参数来控制,当空闲线程的存活时间超过设定值时,线程池会销毁这些线程。

在使用线程池时,如何优雅地处理异常?

  • RunnableCallablerun() 方法中,可以使用 try-catch 语句来捕获并处理任务执行过程中的异常。
  • 此外,可以通过实现 ThreadFactory 接口,在创建线程时自定义线程的异常处理机制。

如何监控和调优线程池的性能?

  • 可以使用线程池提供的监控接口,如 getTaskCount()getActiveCount()getCompletedTaskCount() 来查看线程池的运行状态和执行任务的情况。
  • 可以通过调整线程池的参数,如核心线程数、最大线程数和任务队列的大小,以及合理选择拒绝策略来调优线程池的性能。

在什么情况下应该使用自定义线程池,而不是直接使用线程池工厂提供的默认线程池?

  • 使用自定义线程池可以更好地满足特定的业务需求。例如,对于执行长时间任务的场景,可以为线程池设置较大的最大线程数,以提高任务的处理速度。