背会了常见的几个线程池用法,结果被问翻了

这篇具有很好参考价值的文章主要介绍了背会了常见的几个线程池用法,结果被问翻了。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

分享是最有效的学习方式。

背景

这是张小帅失业之后的第三场面试。

面试官:“实际开发中用过多线程吧,那聊聊线程池吧”。

“有CachedThreadPool:可缓存线程池,FixedThreadPool:定长线程池.......balabala”。小帅暗暗窃喜,还好把这几种线程池背下来了,看来这次可以上岸了。

面试官点点头,继续问到“那线程池底层是如何实现复用的?”

“额,这个....”

寒风中,那个男人的背影在暮色中显得孤寂而凄凉,仿佛与世隔绝,独自面对着无尽的寂寞......

概要

如果问到线程池的话,不好好剖析过底层代码,恐怕真的会像小帅那样被问翻吧。

那么在此我们就来好好剖析一下线程池的底层吧。我们大概从如下几个方面着手:

什么是线程池

说到线程池,其实我们要先聊到池化技术。

池化技术:我们将资源或者任务放入池子,使用时从池中取,用完之后交给池子管理。通过优化资源分配的效率,达到性能的调优。

池化技术优点:

  1. 资源被重复使用,减少了资源在分配销毁过程中的系统的调度消耗。比如,在IO密集型的服务器上,并发处理过程中的子线程或子进程的创建和销毁过程,带来的系统开销将是难以接受的。所以在业务实现上,通常把一些资源预先分配好,如线程池,数据库连接池,Redis连接池,HTTP连接池等,来减少系统消耗,提升系统性能。

  2. 池化技术分配资源,会集中分配,这样有效避免了碎片化的问题。

  3. 可以对资源的整体使用做限制,相关资源预分配且只在预分配是生成,后续不再动态添加,从而限制了整个系统对资源的使用上限。

所以我们说线程池是提升线程可重复利用率、可控性的池化技术的一种。

线程池的使用

多线程发送邮件案例

现在我们有这样一个场景,上层有业务系统批量调用底层进行发送邮件,废话不多,直接上代码:

最终运行输出结果为:

由线程:pool-1-thread-1 发送第:0封邮件
由线程:pool-1-thread-2 发送第:1封邮件
由线程:pool-1-thread-1 发送第:2封邮件
由线程:pool-1-thread-2 发送第:3封邮件
由线程:pool-1-thread-1 发送第:4封邮件
由线程:pool-1-thread-1 发送第:6封邮件
由线程:pool-1-thread-2 发送第:5封邮件
由线程:pool-1-thread-1 发送第:7封邮件
由线程:pool-1-thread-2 发送第:8封邮件
由线程:pool-1-thread-1 发送第:9封邮件

上面的例子中从结果来看是10封邮件分别由两条线程发送出去了,上图可见,我们给ThreadPoolExecutor这个执行器分别指定了七个参数。那么参数的含义到底是什么呢?接下来咱们层层抽丝剥茧。

构造函数说明

大家估计会有疑问,线程池的种类那么多,案例中为什么要用TheadPoolExecutor类呢,其他的种类是由TheadPoolExecutor通过不同的入参定义出来的,所以我们直接拿ThreadPoolExecutor来看。

我们先来看一下ThreadPoolExecutor的继承关系,有个宏观印象:

我们再来看一下ThreadPoolExecutor的构造方法:

下面我们来解释一下几个参数的含义:

  1. corePoolSize: 核心线程数。

  2. maximumPoolSize: 最大线程数。

  3. keepAliveTime: 线程池中线程的最大闲置生命周期。

  4. unit: 针对keepAliveTime的时间单位。

  5. workQueue: 阻塞队列。

  6. threadFactory: 创建线程的线程工厂。

  7. handler: 拒绝策略。

大家对上述的含义初步有个概念。

工作流程概述

看了上面的构造函数字段大家估计也还是优点懵的,尤其是从来没有接触过商品池的小伙伴。所以老猫又撸了一张商品池的大概的工作流程图,方便大家把这些概念串起来。

上图中老猫标记了四条线,简单介绍一下(当然上图若有问题,也希望大家能够指出来)。

  1. 当发起任务时候,会计算线程池中存在的线程数量与核心线程数量(corePoolSize)进行比较,如果小于,则在线程池中创建线程,否则,进行下一步判断。
  2. 如果不满足条件1,则会将任务添加到阻塞队列中。等待线程池中的线程空闲下来后,获取队列中的任务进行执行。
  3. 但是条件2中如果阻塞队列满了之后,此时又会重新获取当前线程的数量和最大线程数(maximumPoolSize)进行比较,如果发现小于最大线程数,那么继续添加到线程池中即可。
  4. 如果都不满足上述条件,那么此时会放到拒绝策略中。

execute核心流程剖析

接下来我们来看一下执行theadPoolExecutor.execute()的时候到底发生了什么。先来看一下源码:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
ctl变量

进入执行源码之后我们首先看到的是ctl,只知道ctl中拿到了一个int数据至于这个数值有什么用,目前不知道,接着看涉及的相关代码,老猫将相关的代码解读放到源码中进行注释。

    //通过ctl获取线程池的状态以及包含的线程数量
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;   // COUNT_BITS = 32-3 = 29
    /**001左移29位
     * 00100000 00000000 00000000 00000000
     * 操作减1
     * 00011111 11111111 11111111 11111111(表示初始化的时候线程情况,1表示均有空闲线程)
     * 换成十进制:COUNT_MASK = 536870911
     */
    private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
    /**
     * 运行中状态
     * 1的原码
     * 00000000 00000000 00000000 00000001
     * 取反+1
     * 11111111 11111111 11111111 11111111
     * 左移29位
     * 11100000 00000000 00000000 00000000
     **/
    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS; //运行中状态  11100000 00000000 00000000 00000000
    private static final int SHUTDOWN   =  0 << COUNT_BITS; //终止状态    00000000 00000000 00000000 00000000
    private static final int STOP       =  1 << COUNT_BITS; //停止       00100000 00000000 00000000 00000000
    private static final int TIDYING    =  2 << COUNT_BITS; //           01000000 00000000 00000000 00000000
    private static final int TERMINATED =  3 << COUNT_BITS; //           01100000 00000000 00000000 00000000
    
    //取高3位表示获取运行状态
    private static int runStateOf(int c)     { return c & ~COUNT_MASK; }  //~COUNT_MASK表示取反码:11100000 00000000 00000000 00000000
    //取出低位29位的值,当前活跃的线程数
    private static int workerCountOf(int c)  { return c & COUNT_MASK; } //COUNT_MASK:00011111 11111111 11111111 11111111
    //计算ctl的值,ctl=[3位]线程池状态 + [29位]线程池中线程数量。
    private static int ctlOf(int rs, int wc) { return rs | wc; } //进行或运算

上面我们针对各个状态以及那么多的二级制表示符有点懵,当然如果不会二进制运算的,大家可以先自己去了解一下二进制的运算逻辑。
通过源码中的英文,我们知道CTL的值其实分成两部分组成,高三位是状态,其余均为当先线程数。如下的图:

上面的图的描述解释,其实也都是英文注释版的翻译,我们再来看一下有了这些状态,这些状态是怎么流转的,英文注释是这样的:

/*** RUNNING -> SHUTDOWN
     *    On invocation of shutdown()
     * (RUNNING or SHUTDOWN) -> STOP
     *    On invocation of shutdownNow()
     * SHUTDOWN -> TIDYING
     *    When both queue and pool are empty
     * STOP -> TIDYING
     *    When pool is empty
     * TIDYING -> TERMINATED
     *    When the terminated() hook method has completed
     * /

上面的描述不太直观,老猫将流程串了起来,得到了下面的状态机流转图。如下图:

写到这里,其实ctl已经很清楚了,ctl说白了就是状态位和活跃线程数的表示方式。通过ctl咱们可以知道当前是什么状态以及活跃线程数量是多少
(设计很巧妙,如果此处还有问题,欢迎大家私聊老猫)。

线程池中的线程数小于核心线程数

读完ctl之后,我们来看一下接下来的代码。

if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true)) return; //添加新的线程
            c = ctl.get(); //重新获取当前的状态以及线程数量
}

继上述的workerCountOf,我们知道这个方法可以获取当前活跃的线程数。如果当前线程数小于配置的核心线程数,则会调用addWorker进行添加新的线程。
如果添加失败了,则重新获取ctl的值。

任务添加到队列的相关逻辑
if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //再次check一下,当前线程池是否是运行状态,如果不是运行时状态,则把刚刚添加到workQueue中的command移除掉
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }

上述我们知道当添加线程池失败的时候,我们会重新获取ctl的值。
此时咱们的第一步就很清楚了:

  1. 通过isRunning方法来判断线程池状态是不是运行中状态,如果是,则将command任务放到阻塞队列workQueue中。
  2. 再次check一下,当前线程池是否是运行状态,如果不是运行时状态,则把刚刚添加到workQueue中的command移除掉,并调用拒绝策略。否则,判断如果当前活动的线程数如果为0,则表明只去创建线程,而此处,并不执行任务(因为,任务已经在上面的offer方法中被添加到了workQueue中了,等待线程池中的线程去消费队列中的任务)
线程池中的线程数量小于最大线程数代码逻辑以及拒绝策略的代码逻辑

接下来,我们看一下最后的一个步骤

/**
 * 进入第三步骤前提:
 * 1.线程池不是运行状态,所以isRunning(c)为false
 * 2.workCount >= corePoolSize的时候于此同时并且添加到queue失败的时候执行
 */
else if (!addWorker(command, false))
            reject(command);
    }

由于调用addWorker的第二个参数是false,则表示对比的是最大线程数,那么如果往线程池中创建线程依然失败,即addWorker返回false,那么则进入if语句中,直接调用reject方法调用拒绝策略了。

写到这里大家估计会对这个第二个参数是false为什么比较的是最大线程数有疑问。其实这个是addWorker中的方法。我们可以大概看一下:

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (int c = ctl.get();;) {
            // Check if queue empty only if necessary.
            if (runStateAtLeast(c, SHUTDOWN)
                && (runStateAtLeast(c, STOP)
                    || firstTask != null
                    || workQueue.isEmpty()))
                return false;

            for (;;) {
                if (workerCountOf(c)
                    >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateAtLeast(c, SHUTDOWN))
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
}

我们很明显地看到当core为flase的时候咱们获取的是maximumPoolSize,也就是最大线程数。

写到这里,其实咱们的核心主流程大概就已经结束了。这里其实老猫也只是写了一个算是比较入门的开头。当然我们还可以在深入去理addWorker的源码。这个其实就交给大家去细看了,篇幅过长,相信大家也会失去阅读的兴趣了,感兴趣的可以自己研究一下,如果说还是有问题的,可以找老猫一起探讨,老猫的公众号:"程序员老猫"。
老猫觉得在上述的源码中比较重要的其实就是ctl值的流转顺序以及计算方式,读懂这个的话,后面一切的源码只要树藤摸瓜即可理解。

Executors线程池模板

我们上述主要和大家分享了比较核心的theadPoolExecutor。除此之外,线程池Executors里面包含了很多其他的线程池模板。
当然这也是小猫直接面试的时候说的那些,其实小猫也就仅仅只是背了线程池模板而已,并不知晓其工作原理。
如下几种:

  1. newCachedThreadPool
    创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  2. newFixedThreadPool
    创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  3. newScheduledThreadPool
    创建一个定长线程池,支持定时及周期性任务执行。

  4. newSingleThreadScheduleExecutor
    创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程会代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newScheduledThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。

  5. newSingleThreadExecutor
    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

多样化的blockingQueue

  1. PriorityBlockingQueue
    它是一个无界的并发队列。无法向这个队列中插入null值。所有插入到这个队列中的元素必须实现Comparable接口。因此该队列中元素的排序就取决于你自己的Comparable实现。

  2. SynchronousQueue
    它是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一个元素的话,那么试图向队列中插入一个新元素的线程将会阻塞,直到另一个新线程将该元素从队列中抽走。同样的,如果队列为空,试图向队列中抽取一个元素的线程将会被阻塞,直到另一个线程向队列中插入了一条新的元素。因此,它其实不太像是一个队列,而更像是一个汇合点。

  3. ArrayBlockingQueue
    它是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。一但初始化,大小就无法修改

  4. LinkedBlockingQueue
    它内部以一个链式结构(链接节点)对其元素进行存储。可以指定元素上限,否则,上限则为Integer.MAX_VALUE。

  5. DelayQueue
    它对元素进行持有直到一个特定的延迟到期。注意:进入其中的元素必须实现Delayed接口。

上述针对这些罗列了一下,其实很多官网上也有相关的介绍,当然感兴趣的小伙伴也可以再去刨一刨里面的源码实现。

拒绝策略

  1. AbortPolicy
    丢弃任务并抛出RejectedExecutionException异常。

  2. DiscardPolicy
    丢弃任务,但是不抛出异常。

  3. DiscardOldestPolicy
    丢弃队列中最前面的任务,然后重新尝试执行任务。

  4. CallerRunsPolicy
    由调用线程处理该任务。

总结

很多小伙伴在用一些线程池或者第三方中间件的时候可能只停留在如何使用上,一旦出了问题或者被人深入问到其实现原理的时候就比较头大。
所以在日常开发的过程中,我们不仅仅需要知道如何去用,其实更应该知道底层的原理是什么。这样才能长立于不败之地。老猫后续也计划出一些关于spring源码阅读系列的连载文章。希望和大家一起进步感兴趣的小伙伴可以加个关注。
公众号:“程序员老猫”

::: block-2
我是老猫,10Year+资深研发老鸟,让我们一起聊聊技术,聊聊人生。
如果有帮到你,求个点赞、关注、分享三连击,谢谢。
:::文章来源地址https://www.toymoban.com/news/detail-783137.html

到了这里,关于背会了常见的几个线程池用法,结果被问翻了的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 连接贝尔金无线路由器时需要注意的几个常见问题

        一、随着使用无线设备连接无线网的情况越来越多,同时涌现出越来越多的无线网络问题,针对个人设备的网络访问控制、为移动访问建立限制和政策、外界干扰对无线网络的影响、无线网络安全以及移动设备数据监督等。 二、不过好在还可以支持客户部署新的无线网络

    2024年02月05日
    浏览(59)
  • 【Java多线程】线程中几个常见的属性以及状态

    目录 Thread的几个常见属性 1、Id 2、Name名称 3、State状态 4、Priority优先级 5、Daemon后台线程 6、Alive存活   ID 是线程的唯一标识,由系统自动分配,不同线程不会重复。 用户定义的名称。该名称在各种调试工具中都会用到。 状态表示线程当前所处的一个情况。和进程一样,线程

    2024年02月19日
    浏览(43)
  • 关于AMC8模拟考试延长到1月19日14点,以及常见的几个新问题

    相信过去的周末两天,很多参加今年AMC8美国数学思维竞赛活动的孩子们都参加了AMC8模拟考试。昨天有家长问六分成长,周末两天因故没能参加要不要紧?如果还想参加怎么办? 不用担心!官方已经把AMC8模拟考试的时间延长到1月19日(星期五)14点了,也就是正式比赛当天下

    2024年01月19日
    浏览(80)
  • Java面试被问了几个简单的问题,却回答的不是很好

    作者: 逍遥Sean 简介:一个主修Java的Web网站游戏服务器后端开发者 主页:https://blog.csdn.net/Ureliable 觉得博主文章不错的话,可以三连支持一下~ 如有需要我的支持,请私信或评论留言! 前言 前几天参加了一个做web开发的面试,被问了几个问题,虽然有些题目比较偏,但是确

    2024年02月08日
    浏览(63)
  • 【线程池】面试被问到线程池参数如何配置时该如何回答

           前言         没有基于业务场景,直接抛出这个问题,等同于耍流氓。         八股文告诉我们CPU密集型就 核心数+1 ,IO密集型就 核心数*2 ,那么真实业务中该怎么去配置呢。         方法论还是有的         1.需要分析线程池执行的任务的特性: CPU 密集型

    2024年02月09日
    浏览(47)
  • 赋的几个发展阶段

    赋,起源于战国,形成于汉代,是由楚辞衍化出来的 ,也继承了《诗经》讽刺的传统。关于诗和赋的区别,晋代文学家陆机在《文赋》里曾说: 诗缘情而绮靡,赋体物而浏亮。 也就是说,诗是用来抒发主观感情的,要写得华丽而细腻;赋是用来描绘客观事物的,要写得爽朗而

    2024年02月07日
    浏览(63)
  • IO的几个模型

    说到I/O模型,都会牵扯到同步、异步、阻塞、非阻塞这几个词,以下讲解这几个词的概念。 阻塞和非阻塞 阻塞和非阻塞指的是一直等还是可以去做其他事。 阻塞(一直等水烧开)(blocking): 调用结果返回之前,调用者被挂起(当前线程进入非可执行状态,在这个状态,CPU不

    2024年02月12日
    浏览(45)
  • SQL中的几个区别

    1:几种JOIN连接方式的区别? 2:几种排序窗口函数的区别? 3:on和where的区别? 4:having和where的区别? 5:union和union all的区别? 6:in和exists的区别? 7:数据库中空字符串、0和NULL的区别? 8:count(1)、count(*)和count(列名)的区别? 1- 几种JOIN连接方式的区别? INNER JOIN(内连

    2024年01月19日
    浏览(34)
  • 关于中断的几个小问题

    1. intel 8259芯片中的IRQ2和int2的区别是什么? 答曰:IRQ2是芯片上的引脚,而int2是中断向量表的第2项,两者有很大区别。 Intel8259A芯片的中断引脚分别为: 主片: 0:8254时钟 1:键盘 2: 从片 3: com2 4:com1 5:声卡 6:软盘 7: lpt打印机 从片: 0:cmos时钟 1:到主片IRQ2的引脚 2:网

    2024年02月07日
    浏览(39)
  • 求数组长度的几个函数

    纯纯小白,有错请指出,谢谢。 1、sizeof函数 对于一个给定数组,如:int arr[]={1,2,3,4,5} , 可以利用 sizeof(arr)/sizeof(arr[0]) 的方式来求字符串长度 , 需要注意的是,sizeof 计算出的字符串长度包括了本身隐含的‘\\0’;对于一个自定义输入的数组,如:int arr[10] , 同样的方法并不能

    2024年02月16日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包