【多线程】线程池源码(一)

计算机
• 阅读 945

上一篇文章讲了有关线程池的一些简单的用法,这篇文章主要是从源码的角度进一步带大家了解线程池的工作流程和工作原理。

首先先来回顾下如何使用线程池开启线程

private static void createThreadByThreadPoolExecutor() {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5,5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
    for (int i = 0; i < 10; i++) {
        MyThread myThread = new MyThread();
        executor.execute(myThread);
    }

可以看到其实没有其它特殊的地方,除了构建线程池的代码,其它最终要的就是executor.execute(myThread) 行代码了。

准备工作

在多线程系列的第一篇文章中提到了线程和进程的状态,线程池同样也有状态,如下:

  • Running: 允许接受新的任务,并且处理队列中的任务
  • Shutdown: 不接受新的任务,但是仍然会处理队列中的任务
  • Stop: 不接受新的任务,不处理队列中的任务,而且中端在运行的任务
  • Tidying: 所有的任务都已经终端,并且工作线程数归0,该状态下的线程都会调用terminated() 函数
  • Terminated:terminated() 函数调用完后就进入了此状态

首先需要知道4个概念WorkerworkersworkQueuetask.

  • 在线程池中有个比较重要的类,那就是Worker ,可以看到其实现了Runnable接口(其实就是工作线程),继承了AbstractQueuedSynchronizer类(俗称AQS,在多线程中是很重要的类)

    【多线程】线程池源码(一)

  • workers 就是Worker 的一个集合,private final HashSet<Worker> workers = new HashSet<Worker>();
  • task :需要执行的任务,也就是execute() 中的参数,实现了Runnable接口
  • workQueue 就是工作队列,就是上一篇文章中线程池构造函数中的工作队列,里面存储的就是需要执行的任务,队列是实现BlockingQueue接口的类,有以下这些实现

    【多线程】线程池源码(一)

execute()

本方法传进去的类是需要实现Runnable接口的,作为一个command 传进去

【多线程】线程池源码(一)

遇到新的任务

  1. 如果工作线程数 < 核心线程数,那么直接加1个worker
  2. 如果线程池是正常的工作状态,并且工作队列能够添加任务,此时需要第二轮判断

    1. 如果线程池因为某种原因不正常了,并且能够成功从工作队列中删除任务,那么直接采取拒绝策略
    2. 如果此时工作线程数为0,此时需要新建一个线程(并且这里创建的是非核心线程)来执行这个任务,为什么是null呢,因为已经把任务放在工作队列里面了。如果新建的worker的firstTask是该任务的话,就会重复执行两次任务。
    3. 其它情况也就是正常情况下,是啥都不干,因为主要目的是把任务放到工作队列中
  3. 如果工作队列已经满了,则需要判断是否能够成功添加一个非核心线程,如果连非核心线程数都满足不了了,就是线程池真的搞不了了,累了,所以也会调用拒绝策略。
注意:核心线程和非核心线程只是语义上的说法,没有本质上的区别

addworker()

addworker 的作用是检查是否可以根据当前池状态和给定界限(核心或最大值)添加新线程 ,并且通过第二个参数来判定是否创建核心线程,当为true的时候就是核心线程,反之就是非核心线程。源码里的注释如下

【多线程】线程池源码(一)

来看看代码具体是如何的

【多线程】线程池源码(一)

  1. 一进来就是一个死循环,这个死循环最主要的目的是确认线程池状态是否正常。如果线程池的状态大于SHUTDOWN,也就是处于STOP、TIDYING或者TERMINATED的时候,线程池都没了,还创建worker干啥,直接返回fasle;当线程池处于SHUTDOWN的时候,又得再次判断:

    1. 传进来的任务如果不为空,那么就不用添加worker的,银行下班了,办理完现在的顾客就不在办理了,不然一直来那不是一直不能下班。
    2. 传进来的任务为空,但是工作队列为空,同样不用添加worker,银行都没有任务了,而且又到点下班了。除了以上3中情况返回false,其它情况不做任何反应,往下走。
  2. 又是一个死循环,首先得到工作线程数如果超过了边界,比如超过了容量、核心线程数或者最大线程数,就不用添加worker了,银行实在是办理不了新的顾客了;当工作线程数正常的情况下,通过CAS来增加工作线程数,如果能增加成功就退出最外层循环。如果增加工作线程失败,那就是其它线程增加了该数量,如果此时线程池的运行状态发生了改变,则重复外层循环,否则就自旋直到成功增加工作线程数。
  3. 到这里就可以说基本扫除了障碍,以传进来的firstTask新建一个Worker,然后获取Worker里的线程。如果线程为null,那么就直接执行添加Worker失败的逻辑,否则就是正常的逻辑。
  4. 先来看正常的逻辑,拿到锁,并且开始又一次的获取线程池的状态

    1. 如果线程是正常运行状态,或者说是关闭状态下firstTask仍然为null,此时如果线程是alive 那么而说明线程已经开启,直接抛出异常。
    2. 正常情况下是wokers集合中添加新的worker元素,并且调整线程池最大值,设置workerAdded标志为true。
    3. 重点 ,当workerAdded为true的时候,开启工作线程,也就是代码中的t.start()
  5. 再来看失败的逻辑 ,是在第3点构造一个Worker对象,获取线程的时候,有可能获取到的线程为空,这样其实就是增加worker失败,返回false,在返回false之前会触发执行失败的逻辑addWorkerFailed() 逻辑。整个的逻辑是比较简单的,就不再花费篇幅去阐述。

【多线程】线程池源码(一)

到这里,整个addWorker()的流程是比较清晰的,值得一提的就是第2行代码中的retry 这个看起来是关键字,但其实不是,仅仅只是一个类似标志位的东西,可以是retry,也可以是abc。

通常是配合for循环来使用,搭配continue和break可以达到goto的效果。比如continue retry; 就是跳到一开始最外层的for循环,break retry; 相当于退出整个循环。写一个最简单的例子大家都能知道了。

public class RetryExample {
    public static void main(String[] args) {
        abc:
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; j++) {
                if (j == 2) {
                    continue abc;
                }
                if (i == 8) {
                    break abc;
                }
                System.out.println("i: " + i + ", j: " + j);
            }
        }
    }
}

输出结果如下图所示

【多线程】线程池源码(一)

创作不易,如果对你有帮助,欢迎点赞,收藏和分享啦!

下面是个人公众号,有兴趣的可以关注一下,说不定就是你的宝藏公众号哦,基本2,3天1更技术文章!!!

【多线程】线程池源码(一)

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
3年前
java各种面试问题
二、Java多线程相关线程池的原理,为什么要创建线程池?创建线程池的方式;线程的生命周期,什么时候会出现僵死进程;说说线程安全问题,什么实现线程安全,如何实现线程安全;创建线程池有哪几个核心参数?如何合理配置线程池的大小?volatile、ThreadLocal的使用场景和原理;
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
Java多线程之线程池7大参数、底层工作原理、拒绝策略详解
Java多线程之线程池7大参数详解目录企业面试题线程池7大参数源码线程池7大参数详解底层工作原理详解线程池的4种拒绝策略理论简介面试的坑:线程池实际中使用哪一个?1\.企业面试题线程池的工作原理,几个重要参数,然后给了具体几个参数分析线程池会怎么做,最后问阻塞队列用是什么?线程池的构造类的方
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Noark入门之线程模型
0x00单线程多进程单线程与单进程多线程的目的都是想尽可能的利用CPU,减少CPU的空闲时间,特别是多核环境,今天咱不做深度解读,跳过...0x01线程池锁最早的一部分游戏服务器是采用线程池的方式来处理玩家的业务请求,以达最大限度的利用多核优势来提高处理业务能力。但线程池同时也带来了并发问题,为了解决同一玩家多个业务请求不被
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
计算机
计算机
Lv1
带烟霞半山斜照影,都变做满川诗兴。
文章
4
粉丝
0
获赞
0