Java-多线程-线程池,面试必问,你都懂了吗?


在这里插入图片描述

概念说明

什么是线程池

    线程池是一种用于管理和复用线程的机制。它由一个线程队列和一组管理线程的方法组成。线程池中的线程可以被重复使用,用于执行提交的任务,而不需要每次都创建和销毁线程。

线程池组成部分

    线程队列:线程池维护一个线程队列,用于存储待执行的任务。当有任务提交到线程池时,线程池会从队列中取出一个空闲的线程来执行任务。
    线程管理器:线程池的线程管理器负责创建、启动和停止线程。它会根据需要动态地创建线程,并将线程添加到线程队列中。当线程池不再需要某个线程时,线程管理器会停止该线程并从线程队列中移除。
    任务接口:线程池中的任务接口定义了待执行的任务的类型。任务可以是实现了Runnable接口的普通任务,也可以是实现了Callable接口的有返回值的任务。
    任务队列:线程池的任务队列用于存储待执行的任务。当有任务提交到线程池时,线程池会将任务加入到任务队列中,等待线程来执行。

优势利弊

线程池优点

    降低资源消耗:线程池可以重复利用已创建的线程,避免了线程的频繁创建和销毁的开销。线程的创建和销毁都需要消耗系统资源,包括CPU、内存等。通过使用线程池,可以将线程的创建和销毁集中在一处,减少了资源的消耗,提高了系统的性能和效率。
    提高响应速度:线程池可以提高任务的响应速度。线程池中的线程是预先创建好的,当有任务到达时,可以立即分配一个空闲的线程来执行任务,而不需要等待线程的创建和启动。这样可以减少任务的等待时间,提高任务的响应速度。
    控制并发线程数量:线程池可以控制并发线程的数量,避免了系统中线程数量过多导致的资源竞争和性能下降问题。通过设置线程池的大小,可以限制同时执行的线程数量,避免系统过载。同时,线程池还可以根据系统的负载情况动态调整线程的数量,保持系统的稳定性和性能。
    提供线程管理和监控功能:线程池提供了一些管理和监控线程的方法,可以更好地控制和优化线程的执行。例如,可以设置线程的优先级、超时时间、线程名称等属性。同时,线程池还提供了一些监控方法,可以获取线程池中线程的状态、执行情况等信息,方便进行线程的调试和优化。

线程池缺点

    1.资源占用:线程池会预先创建一定数量的线程,这些线程会一直存在,即使没有任务需要执行。这样会占用一定的系统资源,例如内存和CPU资源。
    举例:假设一个线程池配置了10个线程,但在某个时间段内只有2个任务需要执行,其他8个线程就会一直空闲占用资源。
    2.线程泄露:线程池中的线程是可以复用的,当一个任务执行完成后,线程会返回线程池并标记为可用状态。然而,如果在任务执行过程中发生了异常,并没有正确地将线程返回线程池,就会导致线程泄露。
    举例:某个任务在执行过程中发生了异常,并没有将线程返回线程池。这样,该线程就无法被复用,会一直占用着系统资源。
    3.阻塞问题:线程池中的线程是有限的,当任务过多时,线程池可能会因为线程不足而无法及时处理任务,导致任务被阻塞。
    举例:线程池中只有5个线程,但同时有10个任务需要执行。前5个任务可以被立即执行,但后面的5个任务需要等待前面的任务执行完成后才能执行,导致任务被阻塞。

原理

线程池主要的任务处理流程

在这里插入图片描述

    i.如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
    ii.如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
    iii.如果向任务队列投放任务失败(任务队列已经满了),说明这个时候任务已经多到爆棚,需要一些“临时工”来执行这些任务了。于是会创建非核心线程去执行这个任务
    iv.如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝

   // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
   private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

    private static int workerCountOf(int c) {
    
    
        return c & CAPACITY;
    }
    //任务队列
    private final BlockingQueue<Runnable> workQueue;

    public void execute(Runnable command) {
    
    
        // 如果任务为null,则抛出异常。
        if (command == null)
            throw new NullPointerException();
        // ctl 中保存的线程池当前的一些状态信息
        int c = ctl.get();

        //  下面会涉及到 3 步 操作
        // 1.首先判断当前线程池中执行的任务数量是否小于 corePoolSize
        // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
        if (workerCountOf(c) < corePoolSize) {
    
    
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 2.如果当前执行的任务数量大于等于 corePoolSize 的时候就会走到这里,表明创建新的线程失败。
        // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态并且队列可以加入任务,该任务才会被加入进去
        if (isRunning(c) && workQueue.offer(command)) {
    
    
            int recheck = ctl.get();
            // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
            if (!isRunning(recheck) && remove(command))
                reject(command);
                // 如果当前工作线程数量为0,新创建一个非核心线程并执行。
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false); //private boolean addWorker(Runnable firstTask, boolean core)
        }
        //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
        // 传入 false 代表增加线程时判断当前线程数是否少于 maxPoolSize
        //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
        else if (!addWorker(command, false))
            reject(command);
    }

为什么要二次检查线程池的状态?
    在多线程的环境下,线程池的状态是时刻发生变化的。很有可能刚获取线程池状态后线程池状态就改变了。判断是否将command加入workqueue是线程池之前的状态。倘若没有二次检查,万一线程池处于非RUNNING状态(在多线程环境下很有可能发生),那么command永远不会执行。

线程池的生命周期

在这里插入图片描述
线程池的生命周期可以分为 RUNNING、SHUTDOWN、STOP、TIDYING 和 TERMINATED 五个状态。

  1. RUNNING:线程池处于运行状态,可以接收新的任务。

    扫描二维码关注公众号,回复: 16504507 查看本文章
  2. SHUTDOWN:调用 shutdown() 方法后,线程池进入 SHUTDOWN 状态,不再接受新的任务,但会将任务队列中的任务全部执行完成。

  3. STOP:调用 shutdownNow() 方法后,线程池进入 STOP 状态,不再接受新的任务,同时会尝试中断正在执行的任务并返回队列中的未执行任务。

  4. TIDYING:当线程池执行完所有任务后,会进入 TIDYING 状态,此时线程池会去清除一些已经无用的资源,如关闭线程池中的线程等。

  5. TERMINATED:当线程池完成清理工作后,线程池会进入 TERMINATED 状态,表示线程池已经结束。

    在线程池的生命周期中,我们需要注意各个状态的转换,以便合理的使用线程池。同时,在结束线程池之前,我们可以通过 awaitTermination() 方法来等待线程池完成所有任务的执行,以确保线程池正常结束。

具体应用

创建

创建线程池的6种方式

方式 描述
Executors.newFixedThreadPool 该方法创建一个固定大小的线程池,所有任务都将以顺序执行。
Executors.newCachedThreadPool 该方法创建一个可以动态调整线程池大小的线程池,适用于执行短期异步任务,当任务完成时将立即释放线程。
Executors.newSingleThreadExecutor 该方法创建一个只有一个工作线程的线程池,所有任务都将依次执行。
Executors.newScheduledThreadPool 该方法创建一个核心线程数固定、可以调度延迟或定期执行的线程池。
ThreadPoolExecutor 该方式提供了更多的线程池自定义选项,并具有更高的灵活性。可以设置核心线程数、最大线程数、任务队列、拒绝策略等参数。
ForkJoinPool 该方式是一种特殊的线程池,适用于执行大量计算密集型任务,例如并行计算、归并排序等。可利用多核CPU提高执行效率。

创建线程池的参数

参数 描述
corePoolSize 线程池的核心大小,即线程池中最少要保留的线程数量
maximumPoolSize 线程池允许创建的最大线程数
keepAliveTime 当线程池中的线程数量超过核心大小时,多余的空闲线程在被回收之前能保持的时间
unit keepAliveTime 的时间单位
workQueue 存储等待执行的任务的阻塞队列
threadFactory 创建新线程的工厂类,用于控制线程的属性和行为
handler 当线程池中的任务队列和线程池都已满时所采取的策略

提交任务

提交任务的两种方式
1.executor:提交Runnable任务,是向线程池中提交无返回值的任务
2.submit:提交Callable任务,是向线程池中提交有返回值的任务

取消任务

时间段 未执行任务 正在执行任务 已完成任务
取消前 可取消或等待执行 可取消或等待执行 不可取消
取消时 已从任务队列中移除 取消执行并返回结果 不可取消
取消后 已从任务队列中移除 已完成执行但不返回结果 已完成执行并返回结果

    注:在取消任务后,若任务已经开始执行,则可能会产生一些副作用,例如释放锁、关闭资源等,但这些操作不会对任务的取消产生影响。

任务拒绝策略

拒绝策略 描述
AbortPolicy 默认策略,抛出 RejectedExecutionException 异常,不执行新任务
CallerRunsPolicy 执行任务的线程自己执行被拒绝的任务
DiscardPolicy 直接丢弃被拒绝的任务,不抛出异常
DiscardOldestPolicy 丢弃队列中最早的任务,将新任务加入队列

    以上策略均可通过实现 RejectedExecutionHandler 接口自定义。其中 AbortPolicy 是默认的拒绝策略,在任务添加到线程池中时,如果线程池已经达到最大容量且队列已满,则会抛出 RejectedExecutionException 异常。其他三种策略则是常见的替代方案,分别为将被拒绝的任务交给调用线程执行、直接丢弃被拒绝的任务、丢弃队列中最早的任务,将新任务加入队列。

关闭操作

线程池的关闭操作分为两种,分别是正常关闭和立即关闭。

  1. 正常关闭

    正常关闭指的是线程池在关闭时会等待所有已提交的任务都执行完毕后再关闭。正常关闭的步骤如下:

(1)调用线程池的 shutdown() 方法,此时线程池进入 shutdown 状态。

(2)线程池将不再接受新的任务,并等待已经提交的任务执行完毕。

(3)当已提交的任务都执行完毕后,线程池会自动关闭,释放所有资源。

  1. 立即关闭

    立即关闭指的是线程池在关闭时不再等待已提交的任务执行完毕而立即关闭,并且会尝试中断线程池中正在执行的任务。立即关闭的步骤如下:

(1)调用线程池的 shutdownNow() 方法,此时线程池进入 shutdownNow 状态。

(2)线程池将不再接受新的任务,并且会尝试中断线程池中正在执行的任务。

(3)中断完成后,线程池会自动关闭,释放所有资源。

    需要注意的是,在立即关闭线程池时,可能会有一些任务因为中断而未能执行完毕,需要根据实际情况进行处理。同时在关闭线程池之后,就不能再提交新的任务,否则会抛出 RejectedExecutionException 异常。

延迟操作

    线程池的延迟操作可以分为执行一次任务和重复性执行任务两种形式。

执行一次任务
    是只执行一次的任务。执行一次任务的延迟操作可以使用ScheduledThreadPoolExecutor类的schedule()方法来实现。该方法接收三个参数:要执行的任务、延迟时间、时间单位。例如:

ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
executor.schedule(new Runnable() {
    public void run() {
        System.out.println("Hello, World!");
    }
}, 1, TimeUnit.SECONDS); // 延迟1秒后执行任务

    上述代码表示,创建了一个核心线程数为1的线程池,然后使用schedule()方法设置了一个延迟1秒后执行的任务。

重复性执行任务
    是需要重复执行的任务。重复性执行任务的延迟操作可以使用ScheduledThreadPoolExecutor类的scheduleAtFixedRate()方法来实现。该方法接收四个参数:要执行的任务、初始延迟时间、执行的周期时间、时间单位。例如:

ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
executor.scheduleAtFixedRate(new Runnable() {
    public void run() {
        System.out.println("Hello, World!");
    }
}, 1, 2, TimeUnit.SECONDS); // 延迟1秒后开始执行任务,每2秒执行一次

    上述代码表示,创建了一个核心线程数为1的线程池,然后使用scheduleAtFixedRate()方法设置了一个延迟1秒后开始执行的任务,每2秒执行一次。

    需要注意的是,当执行的周期时间小于任务执行的时间时,任务可能会出现累积执行的情况,因此需要根据具体需求来设置任务的周期时间。此外,还需要使用shutdown()方法来关闭线程池。

总结

    线程池是一种线程管理的机制,通过封装和复用线程来提高程序的性能和效率。线程池中包含了一些线程和任务队列,它们共同工作来执行任务,并管理线程的创建、销毁和复用。

    在使用线程池时,需要考虑线程池大小、任务队列、拒绝策略等参数,以及线程池的生命周期和状态转换。线程池大小需要根据实际情况进行分析和调整,任务队列需要考虑任务的类型和数量,拒绝策略需要考虑任务被拒绝时的处理方式。

    除了基本的线程池实现,还有一些高级的线程池,如定时线程池、可扩展线程池等。它们可以满足不同的应用场景和需求。

    在线程池的使用过程中,需要注意避免一些常见的问题,如死锁、阻塞、线程泄露等。同时,在线程池的设计和实现中,需要考虑并发访问和线程安全等问题。

    总的来说,线程池是一个非常重要的多线程编程工具,它可以有效地提升程序的性能和效率。使用线程池需要根据实际情况进行合理的配置和优化,同时需要注意线程安全和并发访问等问题。

以下表格是根据线程池的不同维度对不同线程池进行比较:

线程池类型 所属包 核心线程数 最大线程数 任务队列 饱和策略 适用场景
FixedThreadPool java.util.concurrent 固定 固定 无界 抛出异常 适用于执行长期的固定数量的任务。
CachedThreadPool java.util.concurrent 0 Integer.MAX_VALUE SynchronousQueue 抛弃策略 适用于执行大量短期的异步任务。
SingleThreadExecutor java.util.concurrent 1 1 无界 抛出异常 适用于串行执行所有任务的场景。
ScheduledThreadPool java.util.concurrent 固定 固定 有界 抛弃策略 适用于定时执行任务和周期性执行任务的场景。
ThreadPoolExecutor java.util.concurrent 可配置 可配置 可配置 可配置 适用于各种场景,可以根据实际情况进行配置。

    需要注意的是,上述表格中的配置仅为线程池的默认配置,实际应用中可以根据实际情况进行调整和优化。同时,饱和策略的选择也需要根据实际情况进行选择,以保证线程池的正常运行。

猜你喜欢

转载自blog.csdn.net/aqiuisme/article/details/132012723