通过生活案例快速 Get 线程池七个参数和工作原理

454 阅读8分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

祝 我们 亲爱的祖国生日快乐,祖国在一天天的变强大,我们也要加油哦,我爱你,中国!!!

如果信仰有颜色,我想那一定是红色。

也祝 xdm 国庆快乐 万事胜意!!!

这次用一个生活案例来让你快速的 get 到线程池的工作原理和七个参数的作用。

最近在复习面试题,巩固一下自己。学完看看面试题,可以很有效的知道的不足。也是为以后做准备。

你好,我是博主宁在春

希望文章能够让你有所收获,也让我们也一起努力!!!

本文主要针对线程池的七个参数及工作原理做讲解。

阅读完本文能够通过自己的语言简单阐述线程池的工作原理和画出原理图。

一、面试题

在看很多Java面试题相关的文章或者博客中,对于线程池都会有这么几个常见连环问题。

👨‍💻面试官:

  1. 平时工作中使用到线程了吗?线程池的优势是什么?
  2. 请介绍使用线程池的方式。
  3. 可以给我分别介绍一下线程池的七个参数的概念和作用吗?、
  4. 线程池的工作原理是什么的?画一下线程池的工作原理图。
  5. 为什么不建议使用Executors创建线程,而使用ThreadPoolExecutor实现类来创建线程?
  6. 线程池如何配置合理的线程数。

问题,都是一步一步深入。

我们在回答的时候,第一要做到的就是尽可能的让面试官往自己更加有把握的地方问过去。


二、线程池的七个参数

我们平常在使用线程池时,不是使用Executors工具类来创建的,而是显式的使用ThreadPoolExecutor来创建的。

原因:

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

来自于阿里的Java开发手册

接下来来看我们今天的重点哈:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
    }

一个一个参数分析:

  1. corePoolSize – 核心线程数。要保留在池中的线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut

  2. maximumPoolSize – 池中能够容纳同时执行的最大线程数,此值必须大于等于1.

  3. keepAliveTime – 当线程数大于核心数时,这是多余空闲线程在终止前等待新任务的最长时间。

    当前线程池数量超过corePoolSize时,当空闲时间达到KeepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。

  4. unit keepAliveTime参数的时间单位

  5. workQueue – 用于在执行任务之前保存任务的队列。 这个队列将只保存execute方法提交的Runnable任务。

  6. threadFactory – 执行程序创建新线程时使用的工厂 ,一般用默认即可。

  7. handler – 执行被阻塞时使用的处理程序,因为达到了线程边界和队列容量。平常称为拒绝策略。

看完这个概念一下肯定是没啥感觉的,我会用一个生活中的案列来讲解线程池的工作原理,确保大家都能理解。

三、线程池的工作原理

3.1、原理图

image-20210926142258014

先说说这张图:

1)当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:

  • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
  • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入阻塞队列中,进行排队等待;
  • 如果这个时候队列满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  • 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

2)当一个线程完成任务时,它会从队列中取下一个任务来执行

3)当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:

  1. 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。
  2. 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

4)拒绝策略,Jdk默认的拒绝策略有以下四种:

  1. AbortPolicy: 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
  2. CallerRunsPolicy: 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。
  3. DiscardPolicy: 直接丢弃.
  4. DiscardOldestPolicy: 触发拒绝策略时,只要线程池没有关闭的话,丢弃阻塞队列 workQueue中最老的任务,并将新任务加入

上面这个内容我第一次看的时候,也稍稍有些懵,学习的时候懂,过段时间再看又是陌生人啦。

这次为了让记忆更为深刻,举一个特别形象生动的例子。源自于 尚硅谷--周阳老师

3.2、生活案例

相信我们大家肯定都去过银行哈。这次就是以银行来举例的。先看看场景

image-20210926145025681

我来解释下哈:

  1. 一名顾客来到了银行,发现没人在办理业务,直接就到值班1号窗口开始办理业务。

  2. 接着又来了一名顾客,2号窗口没人,就走到了值班2号窗口开始办理业务。

  3. 紧接着又来了第三名、第四名、第五名顾客,现在值班1、2号窗口都正在办理业务,他们就坐在等候区开始等待。

  4. 但是这时候又来了第六名、第七名、第八名顾客。这时候,等候区已满,大堂经理看到这个情况,马上告诉行长,需要增加临时办理窗口。

  5. 问题来了?这个时候增添的临时窗口3、4、5会让谁先去办理业务呢???

    是后来的第六、七、八名顾客,还是之前就在等候区的第三、四、五名顾客呢??

  6. 会处理刚进来的第六、七、八名顾客。

  7. 原因:在上文我说过,当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:

    如果这个时候队列满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;所以会立刻处理第6、7、8名顾客业务。

  8. 继续,如果接着还有很多顾客过来处理业务,这时候大堂经理看到这种情况,就触发拒绝策略。(这种情况就是陌生的哈,那个经理敢把顾客往外赶哈)

接下来我们用代码来模拟这个场景哈。

四、通过代码示例来进一步分析参数

设置的数据就按照图上的来,

corePoolSize核心线程数为:2,maximumPoolSize最大线程数为:5,keepAliveTime:等待时间为:3秒

workQueue阻塞队列为:3,ThreadFactory线程的创建方式就使用默认的:Executors.defaultThreadFactory()

RejectedExecutionHandler拒绝策略:都会测试一遍。

    public static void main(String[] args) {

        BlockingDeque<Runnable> workQueue = new LinkedBlockingDeque<Runnable>(3);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2,
                5,
                3, TimeUnit.SECONDS,
                workQueue,
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i <8 ; i++) {
            final  int temp=i;
            executor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"\t"+"执行第 "+temp+" 个任务!!");
            });
        }
    }

这个代码里设置的拒绝策略为:new ThreadPoolExecutor.AbortPolicy(),超过maximumPoolSize+workQueue之和的数据。就会直接抛出异常,停止运行。

当设置为8的时候,还是可以正常的,我们调到9个任务来试一试。

image-20210926153721414

当我们向上调整上,任务超过最大数,就会触发拒绝策略。

image-20210926154905015

将策略改为new ThreadPoolExecutor.DiscardPolicy());直接就抛弃了第八个任务。

image-20210926162943707

策略改为:new ThreadPoolExecutor.CallerRunsPolicy(),当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。

image-20210926163750302

还有最后一个DiscardOldestPolicy: 触发拒绝策略时,只要线程池没有关闭的话,丢弃阻塞队列 workQueue中最老的任务,并将新任务加入

五、自言自语

你好,我是博主宁在春主页

希望本篇文章能让你感到有所收获!!!

我们:待别日相见时,都已有所成

无论在外如何如何 还是想要回到家。

车站它要是能够记录故事,他应该记录过许许多多的悲欢离合吧。

c21c405d6d7211b8ed76549c97ee53fa.jpg