【并发专题】线程池 连环炮

本问题对应的项目链接:高频题回答模版训练

1、为什么要使用线程池?线程池是怎么提升效率的?

2、简单说一下线程池都有哪些可选参数?

3、线程数是越大越好吗?讲一下核心线程数与最大线程数之间的关系?

4、运行的时候,核心线程数能不能修改?

5、一个任务进来后,线程池是怎么处理的?

6、线程池的拒绝策略有哪几种?讲一下常见的几种

7、平时可以怎么创建一个线程池?

8、线程池有哪些关闭方式?他们的区别?

共以下 5 个回答

  • 終达·斯维达尼亚 永久会员 2024年3月4日 下午10:02

    1、为什么要使用线程池?线程池是怎么提升效率的?

    如果说一个进程中需要使用多个线程,而系统对线程的新建和销毁都比较消耗性能,因此可以使用线程池,通过一次性批量新建若干线程,使用后不销毁而是进入等待状态,有任务就去执行。从而降低资源消耗。

    2、简单说一下线程池都有哪些可选参数?

    核心线程数:该线程被创建后就不会被销毁,而是等待任务的分配,约等于系统最小线程数

    工作线程数:如果系统中的并放需求激增,大于了我们初始设置的核心线程数,则我们可以创建临时线程,等过了业务高峰期直接销毁。

    等待时间:工作线程等待多久销毁

    任务丢弃处理逻辑:如果线程数赶不上并发的要求,导致任务队列已满,就需要定义一个处理溢出任务的逻辑

    线程工程:定义线程如何创建

    任务队列:定义什么样的任务排队队列

    3、线程数是越大越好吗?讲一下核心线程数与最大线程数之间的关系?

    线程数如果过大则会有大量频繁的系统上下文切换的过程,会影响性能。核心线程数+工作线程数=最大线程数。

    4、运行的时候,核心线程数能不能修改?

    可以,ThreadPoolExecutor 类提供了 setCorePoolSize( ),可以在线程池运行时动态修改核心线程数。如果我们调用这个方法尝试减少核心线程数,那么就会中断一部分空闲线程。如果是增加,则需要根据当前工作队列是否有任务来决定是否增加核心线程。

    5、一个任务进来后,线程池是怎么处理的?

    首先先判断核心线程数是否已满,如果未满则创建一个核心线程去执行。如果核心线程已满,则看任务队列是否已满,如果未满则放入队列中排队。如果任务队列已满,则看目前的线程是否超过了最大线程数,未超过则创建一个工作线程去处理任务。如果达到了最大线程数,则执行任务拒绝执行策略。

    6、线程池的拒绝策略有哪几种?讲一下常见的几种

    • 中止策略:如果队列已满则抛出异常。这种策略适合处理关键业务,以便在系统无法承载更多并发量时能够及时通过异常发现问题。
    • 丢弃策略:直接将任务抛弃不处理。这种策略适用于一些不太重要或无关紧要的任务。
    • 弃老策略:丢弃队列中最前面的任务。适用于处理重要且必须执行的任务,确保任务进入队列。
    • 调用者运行策略:由试图将任务放入队列中的线程去处理。(返回给你自己处理)

    7、平时可以怎么创建一个线程池?

    最好是用 ThreadPoolExecutor 创建,不推荐使用 Executors 创建。因为从 Executors 返回的线程池对象,请求队列的长度或者允许创建的线程数为 Integer.MAX_VALUE,可能会堆积大量的请求或者线程,导致内存溢出。

    8、线程池有哪些关闭方式?他们的区别?

    有两种方式,使用 shutdown( ) 或者 shutdownNow( ) 来关闭线程池。shutdown( ) 只是线程池的状态设置成 SHUTDOWN,然后中断所有没有执行任务的线程,而 shutdownNow( ) 是先将线程池状态设为 STOP,然后尝试停止所有的正在执行任务的线程。 当所有的任务都执行完后,才真正表示线程池关闭成功。

    0 赞同 0 条回复
  • LilyBoy 永久会员 2023年9月16日 下午5:15

    1、为什么要使用线程池?线程池是怎么提升效率的?
    使用线程池可以避免手动创建线程带来的难以管理和频繁创建开销大的缺点。
    降低资源消耗。通过重复利用已经创建的线程降低线程创建和销毁造成的消耗。
    提高响应速度。当任务到达时,任务可以不需要等到线程创建就可以立即执行。
    提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控;

    2、简单说一下线程池都有哪些可选参数?
    ThreadPoolExcutor3个最重要的参数
    – corePoolSize:任务队列未达队列容量时,最大可以同时运行的线程数量
    – maximumPoolSize:任务队列中存放的任务达到队列容量时,当前可以同时运行的线程数量变为最大线程数
    – workQueue:新任务来时会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务会被存放在队列中;
    其它常见参数
    – keepAliveTime:线程池中的线程数量大于corePoolSize时,若这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待时间超过了keepAliveTime才会被回收销毁;
    – unit:时间单位
    – threadFactory:线程工厂,用来创建线程,一般默认即可
    – handler:饱和策略,当提交的任务过多不能及时处理时,我们可以定制策略来处理任务

    3、线程数是越大越好吗?讲一下核心线程数与最大线程数之间的关系?
    不是,如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的,CPU 根本没有得到充分利用。如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

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

    如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行.
    如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。

    ***4、运行的时候,核心线程数能不能修改?
    可以。
    corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
    比如,ThreadPoolExecutor提供了setCoreThreadSize()设置核心线程数

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

    另外,美团的方式是自定义了一个叫做 ResizableCapacityLinkedBlockIngQueue 的队列(主要就是把LinkedBlockingQueue的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。

    5、一个任务进来后,线程池是怎么处理的?(非核心线程)
    如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务threadFactory.newThread()。
    如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行workQueue.offer()。
    如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务threadFactory.newThread()。
    如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用RejectedExecutionHandler.rejectedExecution()方法

    6、线程池的拒绝策略有哪几种?讲一下常见的几种
    若当前同时运行的线程数量达到最大线程数并且队列也已经被放满了任务时,定义一些饱和策略
    – AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理;
    – CallerRunsPolicy:将任务回退给调用者,使用调用者的线程来执行任务;
    – DiscardOldestPolicy:丢弃掉队列中最靠前的任务,并执行当前任务;
    – DiscardPolicy:不处理新任务,直接丢掉;

    7、平时可以怎么创建一个线程池?

    newFixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
    newSingleThreadExecutor:该方法返回一个只有一个线程的线程池。若多余的一个任务被提交到线程池中,待有线程空闲时,按照先入先出的顺序执行队列中的任务。
    newCachedThreadPool:该方法返回一个可以根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
    newScheduleThreadPool:该方法返回一个用来给定的延迟后运行任务或者定期执行任务的线程池。

    ****8、线程池有哪些关闭方式?他们的区别?
    shutdown()作用: 停止接受新任务, 但是正在执行和队列里面的任务都会完成执行, 都完成后,才是真正的停止!线程池标志位设为SHUTDOWN
    shutdownNow()作用: 停止接受新任务, 且队列的任务清空. 对于正在执行的任务, 可能会停止,可能会执行完成.线程池标志位设为STOP, 正在执行的任务会想用interrupt中断.

    0 赞同 0 条回复
  • 小肸 永久会员 2023年8月16日 下午10:39

    问题背景

    现有系统A、B,A是属于我们的系统,B是属于第三方系统,为了减少A和B的耦合度,A向B第一次请求数据后就将数据存入自己的系统中,后续在自己的系统进行数据操作。所以A需要与B进行网络通信,拉取数据并上传到自己的文件服务器上,同时将数据信息写入到数据库中记录。

    问题描述

    A可能会向B一次请求几千个小文件,这个时候A向B进行网络通信拉取数据必须是异步执行的,如果在当前线程进行会导致前端一直等待后端响应,用户必须等待系统A拉取数据后才可以进行其它操作。

    解决思路

    把A向B拉取数据的操作作为一个任务,交给一个线程在后台执行,直接返回响应给前端。

    具体实现

    1.创建一个线程池,用来管理任务和线程。

    为什么使用线程池?
    线程池本来就是Java为我们提供用来管理线程和任务的工具,提供了很多可选参数,比如核心线程数,最大线程数,任务队列类型等供开发者配置合适的线程池适用于不同的业务场景,降低资源浪费和提高响应速度。比如:
    a.将任务放入一个队列,让空闲线程自己去任务队列中取出任务执行,而不是一个线程执行完一个任务就被销毁了,也不用新建一个线程去执行新任务。避免创建和销毁不必要的线程而造成资源和性能损耗。
    b.具有良好的伸缩性。线程的数量会随着请求数量增减而增减。在一个线程的空闲时间超过存活时间时,会被自动销毁。避免占用资源。当新任务被加入任务队列时:(1)如果没有空闲线程,(1.a)当前线程数小于核心线程数时,线程池会创建一个新线程执行任务;(1.b)当前线程数大于等于核心线程数,(1.b.1)任务队列如果是无界队列,新任务会被加入队列等待执行,(1.b.2)任务队列如果是有界队列,并且未满,新任务会被加入队列等待执行;(1.b.3)任务队列是有界队列并且队列已满,如果当前线程数小于最大线程数,线程池会创建一个线程执行新任务,否则线程池会执行拒绝策略。(分别是直接丢弃新任务不做任何通知、丢弃新任务并抛出异常、将新任务交给当前线程执行、丢弃任务队列中最早的任务再将新任务入队)。

    代码如下(采用单例的饿汉式)

    @Component
    public class KPThreadPoolUtil {
        public static int corePoolSize;
    
        public static int maxPoolSize;
    
        public static long keepAliveTime;
    
        public static int queueCapacity;
        private static BlockingQueue<Runnable> bqueue;
        private static ThreadPoolExecutor pool;
    
        private KPThreadPoolUtil() {}
    
        public static ThreadPoolExecutor getPool() {
            return pool;
        }
    
        static {
            YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean();
            factoryBean.setResources(new ClassPathResource("application.yml"));
            Properties properties = factoryBean.getObject();
            corePoolSize = Integer.parseInt(properties.getProperty("jzxx.threadpool.core-pool-size"));
            maxPoolSize = Integer.parseInt(properties.getProperty("jzxx.threadpool.max-pool-size"));
            keepAliveTime = Long.parseLong(properties.getProperty("jzxx.threadpool.keep-alive-time"));
            queueCapacity = Integer.parseInt(properties.getProperty("jzxx.threadpool.queue-capacity"));
            bqueue = new ArrayBlockingQueue(queueCapacity);
            pool = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS,
                    bqueue, new ThreadPoolExecutor.CallerRunsPolicy());
            pool.prestartAllCoreThreads();
        }
    }
    

    线程数的数量设置:
    一般有一个公式:
    线程数 = CPU核数 * (1 + CPU的IO时间/CPU的计算时间)。可以理解为当一个线程执行一次IO操作时,所有线程正好完成了一次CPU计算操作,这样CPU的利用率就是100%。

    任务队列的选择:
    ArrayBlockingQueue(因为架构上还有一层负载均衡,所以任务数不会很多,容量确定为500,选择使用ArrayBlockingQueue)
    LinkedBlockingQueue
    SynchronousQueue
    PriorityBlockingQueue

    饱和策略的选择:选择CallerRunsPolicy策略。宁可让用户多等一会儿,也不能返回成功响应给用户。因为后续操作都是依赖于系统中文件服务器和数据库的数据进行的。

    如何关闭线程池
    shutdown()。没执行完的任务继续执行。
    shutdownNow()。强制中断没有执行完的任务并返回。
    isShutdown()。根据状态位判断。
    isTerminaed()。根据是否有未关闭的任务判断。
    prestartCoreThread()。预热一个核心线程。
    prestartAllCoreThread()。预热所以核心线程。

    0 赞同 0 条回复
  • Gary 永久会员 2023年7月11日 上午9:54

    1、为了实现对线程的复用,减少线程重复创建和销毁带来的开销。java线程和操作系统的线程是一对一的关系,每创建一个java线程,就要在操作系统创建一个对应的线程,java线程的创建、切换、销毁都需要调用操作系统的API,开销比较大。线程池也可以统一调优和管理线程,提高资源的利用率。

    2、核心线程数、线程总数、线程工厂(定义线程如何创建)、非核心线程空闲时间(没有任务的时候,空闲超过了多长时间就要销毁)、时间单位、拒绝策略(任务执行失败或者线程不够用,无法接收任务的时候,应该执行的逻辑)、任务队列(线程在等待任务的时候,应该在什么样的队列里面等,例如有界队列、无界队列、优先队列、延迟队列等)

    3、根据CPU的核心数、任务类型比如CPU密集型或者IO密集型、任务执行时间来设置。有个公式可以参考,最佳线程数 = CPU核心数+1*(任务等待时间/任务执行时间)。最大线程数=核心线程数+非核心线程数。接收任务的时候,先创建核心线程,核心线程满了,再创建非核心线程,如果都满了,就执行拒绝策略。线程池初始化的时候,不会先创建线程,而是等第一个任务来的时候,才创建。

    4、可以修改。如果改大的话,新任务会新建核心线程;如果改小,多余的核心线程会在下一个空闲周期销毁。

    5、先看看有没有空闲的核心线程,没有的话如果核心线程还创建得不够,就创建多一个核心线程来执行这个任务。如果核心线程够了,就进入任务队列等待,如果任务队列满了,看看有没有空闲的非核心线程,有的话就给它执行,没有的话看看还能不能创建,可以创建的话就给它执行,不能的话说明已经达到最大线程数,此时要执行拒绝策略

    6、四种默认的拒绝策略方式:放弃任务抛异常、直接丢弃任务不做任何处理、丢弃最早提交的任务,然后重新提交新的任务、让提交任务的线程来执行任务

    7、用ThreadPoolExecutor,指定好上面说的这些核心参数,就能创建一个线程池

    8、两种关闭方式,shutdown()和shutdownNow()。
    shutdown是把线程池的状态设置成shundown,不再接收新的任务,但是会先把当前正在进行的任务完成;
    shutdownNow是把线程池的状态设置成stop,会尝试马上停止当前所有正在进行的任务,并返回等待执行任务的列表,但它并不保证百分百可以把任务停止

    0 赞同 0 条回复
  • 秊 永久会员 2023年7月7日 下午5:37

    1、为什么要使用线程池?线程池是怎么提升效率的?

    使用线程池可以避免手动创建线程带来的难以管理和频繁创建开销大的缺点。合理的使用线程池可以降低资源消耗,提高响应速度和提高线程的可管理性。

    具体来说,线程池可以重复利用已创建的线程,来降低线程创建和销毁带来的开销;同时也可以使任务到达时就可以立刻执行,响应速度快;线程池还可以对创建的线程进行统一分配,调度和监控。

    2、简单说一下线程池都有哪些可选参数?

    Java 可以使用 ThreadPoolExecutor 类来手动创建线程池,它的构造参数有七个,有:线程池的核心线程数和最大线程数,空闲线程的存活时长和时长单位,存放任务的阻塞队列,线程工厂和拒绝执行策略。

    其中存放任务的阻塞队列常见的有四种可选,前两种分别是基于数组或链表的队列,都按 FIFO 排序任务。第三种是不存储元素的阻塞队列,每个任务的加入都必须阻塞等待,直到腾出来线程执行。第四种是一个具有优先级的无限阻塞队列。

    3、线程数是越大越好吗?讲一下核心线程数与最大线程数之间的关系?

    不是的,如果程序是 CPU 密集型任务,线程数不能太大,避免线程间的频繁切换;如果程序是 IO 密集型任务,可以适当增加线程数,来充分利用 CPU 等待 IO 的时间,提高吞吐量。但即使是 IO 密集型任务线程数也不能过大,否则也会出现线程频繁切换带来的额外开销。

    核心线程数表示线程池中保持活动状态的线程数量,即空闲线程的最小数量,这些线程执行完任务后不会被销毁。

    最大线程数是指线程池中允许的最大线程数量,包括核心线程数和非核心线程数。非核心线程数是在任务队列已满且核心线程数已满的情况下,创建的额外线程数量,这些线程在执行完任务后就会被销毁。

    4、运行的时候,核心线程数能不能修改?

    可以,ThreadPoolExecutor 类提供了 setCorePoolSize( ),可以在线程池运行时动态修改核心线程数。如果我们调用这个方法尝试减少核心线程数,那么就会中断一部分空闲线程。如果是增加,则需要根据当前工作队列是否有任务来决定是否增加核心线程。

    5、一个任务进来后,线程池是怎么处理的?

    首先需要判断核心线程池是否已满,如果没满则创建一个核心线程执行任务。

    如果核心线程池里的线程都在执行任务,接着要判断工作队列是否已满,如果没满则将这个任务添加到工作队列中。

    如果工作队列也满了,还需判断线程池中所有的线程是否超出了最大线程数,如果没有,则创建一个非核心线程执行任务。

    如果整个线程池都满了,则需要交给创建线程时指定的「拒绝执行策略」去处理。

    6、线程池的拒绝策略有哪几种?讲一下常见的几种

    常见的有四种策略:

    第一种是异常策略(AbortPolicy),直接抛出异常。

    第二章是调用者运行策略(CallerRunsPolicy),将该任务交给调用者所在的线程去执行。

    第三种是丢弃最久策略(DiscardOldestPolicy),丢弃队列中最早提交的任务,并执行当前任务。

    第四种是直接丢弃策略(DiscardPolicy),直接丢弃不处理。

    7、平时可以怎么创建一个线程池?

    最好是用 ThreadPoolExecutor 创建,不推荐使用 Executors 创建。因为从 Executors 返回的线程池对象,请求队列的长度或者允许创建的线程数为 Integer.MAX_VALUE,可能会堆积大量的请求或者线程,导致 OOM。

    8、线程池有哪些关闭方式?他们的区别?

    有两种方式,使用 shutdown( ) 或者 shutdownNow( ) 来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程。但也有一定的区别,shutdown( ) 只是线程池的状态设置成 SHUTDOWN,然后中断所有没有执行任务的线程,而 shutdownNow( ) 是先将线程池状态设为 STOP,然后尝试停止所有的正在执行任务的线程,并返回等待执行任务的列表。

    无论调用这两个方法的哪一个,isShutdown( ) 都会返回 true。当所有的任务都执行完后,才真正表示线程池关闭成功,此时 isTerminaed( ) 返回 true。实际中如果期望此时正在执行的任务继续执行完,可以用 shutdown( ),反之可以用 shutdownNow( )。

    9、demo

    public class ThreadPoolExample {
        public static void main(String[] args) {
            int corePoolSize = 5; // 核心线程数
            int maxPoolSize = 10; // 最大线程数
            long keepAliveTime = 5000; // 线程空闲时间
            BlockingQueue workQueue = new ArrayBlockingQueue<>(10); // 任务队列
            ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂
            RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略
    
            // 创建 ThreadPoolExecutor 对象
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                    corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS,
                    workQueue, threadFactory, handler);
    
            // 提交任务给线程池执行
            for (int i = 0; i < 12; i++) {
                Runnable worker = new WorkerThread("Task " + (i + 1));
                executor.execute(worker);
            }
    
            // 使用 shutdown() 关闭线程池, 大家可以自己试试 shutdownNow() 的效果
            System.out.println("--Start to shutdown the pool!");
            executor.shutdown();
            while (!executor.isTerminated()) {
                // 等待所有任务完成
            }
    
            System.out.println("--All tasks are completed.");
        }
    }
    
    class WorkerThread implements Runnable {
        private final String taskName;
    
        public WorkerThread(String taskName) {
            this.taskName = taskName;
        }
    
        @Override
        public void run() {
            System.out.println("Start executing: " + taskName);
            try {
                // 模拟任务执行时间
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Finished executing: " + taskName);
        }
    }
    

    控制台输出:
    Start executing: Task 2
    Start executing: Task 1
    Start executing: Task 3
    Start executing: Task 4
    Start executing: Task 5
    --Start to shutdown the poo
    // 等待了 2s
    Finished executing: Task 2
    Finished executing: Task 3
    Finished executing: Task 1
    Finished executing: Task 4
    Start executing: Task 6
    Finished executing: Task 5
    Start executing: Task 8
    Start executing: Task 7
    Start executing: Task 9
    Start executing: Task 10
    // 等待了 2s
    Finished executing: Task 7
    Finished executing: Task 9
    Finished executing: Task 6
    Finished executing: Task 8
    Start executing: Task 12
    Start executing: Task 11
    Finished executing: Task 10
    // 等待了 2s
    Finished executing: Task 12
    Finished executing: Task 11
    --All tasks are completed.

    0 赞同 0 条回复

# 回答此问题

后才能回答