线程池底层原理详解与源码分析

【1】为什么要使用线程池?

示例演示:

示例结果:

采用每次都开一个线程的结果是292毫秒,而线程池的是69毫秒。(随着业务次数的增多这个数值的差距会越大)

示例说明:

如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,服务器在创建和销毁线程上花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。

如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,如此一来会大大降低系统的效率。可能出现服务器在为每个请求创建新线程和销毁线程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多。(说明了我们什么时候使用线程池:1.单个任务处理时间比较短;2.需要处理的任务数量很大;)

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

【2】线程池的介绍

(1)线程池优势

1.重用存在的线程,减少线程创建,消亡的开销,提高性能

2.提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

3.提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

(2)常见线程池

1.newSingleThreadExecutor :单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务

2.newFixedThreadExecutor(n) :固定数量的线程池,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行

3.newCacheThreadExecutor(推荐使用) :可缓存线程池, 当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。

4.newScheduleThreadExecutor:大小无限制的线程池,支持定时和周期性的执行线程

5.常见线程池的说明

在阿里的开发手册中其实不推荐我们使用默认的线程池,为什么?

【1】Executors 返回的线程池对象的弊端如下:

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

【2】其次newCacheThreadExecutor,没有核心线程数,且非核心线程数是最大值,不断创建线程容易出现CPU100%的问题。

(3)默认线程池

1.ThreadPoolExecutor

1)说明

实际上不管是newSingleThreadExecutor,newFixedThreadExecutor还是newCacheThreadExecutor,他们都是使用ThreadPoolExecutor去生成的。

只不过由于参数不同导致产生的线程池的不同,因此,我们常使用是ThreadPoolExecutor去自建自己想要的线程池。

2)参数解析

1.corePoolSize
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到 阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

2.maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;

3.keepAliveTime
线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;

4.unit
keepAliveTime的单位;

5.workQueue
用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
4、priorityBlockingQuene:具有优先级的无界阻塞队列;

6.threadFactory
它是ThreadFactory类型的变量,用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。

7.handler

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
上面的4种策略都是ThreadPoolExecutor的内部类。
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。(自定义的才是最常用的)

【3】线程池相关的类分析

1.ExecutorService接口与Executor接口

2.抽象类AbstractExecutorService

3.ThreadPoolExecutor类

4.ScheduledThreadPoolExecutor类

5.问题点

1)execute方法与submit方法的区别?

【1】最明显的就是 :

void execute() //提交任务无返回值
Future submit() //任务执行完成后有返回值

【2】另外一个不明显的就是队列的提交方法(add【ScheduledThreadPoolExecutor类中使用】与offer【ThreadPoolExecutor类中使用】)

明显当队列满了的时候,add方法会抛出异常,而offer不会。

【4】线程池的状态分析

1.线程池存在5种状态
1)RUNNING = ‐1 << COUNT_BITS; //高3位为111 运行状态
2)SHUTDOWN = 0 << COUNT_BITS; //高3位为000 关闭状态
3)STOP = 1 << COUNT_BITS; //高3位为001 停止状态
4)TIDYING = 2 << COUNT_BITS; //高3位为010 整理状态
5)TERMINATED = 3 << COUNT_BITS; //高3位为011 销毁状态

2.状态说明

1、RUNNING
(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(02) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!

2、 SHUTDOWN
(1)状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2)状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

3、STOP
(1)状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2)状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4、TIDYING
(1)状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING 状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理; 可以通过重载terminated()函数来实现。
(2)状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

5、 TERMINATED

(1)状态说明:线程池彻底终止,就变成TERMINATED状态。
(2)状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
进入TERMINATED的条件如下:
线程池不是RUNNING状态;
线程池状态不是TIDYING状态或TERMINATED状态;
如果线程池状态是SHUTDOWN并且workerQueue为空;
workerCount为0;
设置TIDYING状态成功。

3.汇总

默认情况下,如果不调用关闭方法,线程池会一直处于 RUNNING 状态,而线程池状态的转移有两个路径:当调用 shutdown() 方法时,线程池的状态会从 RUNNING 到 SHUTDOWN,再到 TIDYING,最后到 TERMENATED 销毁状态;当调用 shutdownNow() 方法时,线程池的状态会从 RUNNING 到 STOP,再到 TIDYING,最后到 TERMENATED 销毁状态。

4.图示

【5】线程池的源码解析

1.针对自定义线程池的运行分析

1)示例代码:

2)示例结果:

3)示例疑问:

输出的顺序并不是预想的1-5,6-10,11-15,16-20。反而是1-5,16-20,6-10,11-15。(深入源码查探原因)

2.针对自定义线程池ThreadPoolExecutor类的运行分析

1)ThreadPoolExecutor类重要属性 private final AtomicInteger ctl

2)ThreadPoolExecutor类#execute方法【这里涉及到一个概念,提交优先级: 核心线程>队列>非核心线程】

在正常运行状态下,线程池:核心线程执行任务-》塞入队列-》非核心线程执行任务。

体现了在并发不激烈的情况下,尽量减少创建线程的操作,用已有的线程。而且核心线程数并不是提前创建的,而是用到的时候才会创建。而且核心线程数不满,优先以创建线程来执行任务。

逻辑展示

3)ThreadPoolExecutor类#addWorker方法

Worker继承了AQS,使用AQS来实现独占锁的功能。为什么不使用ReentrantLock来实现呢?

可以看到tryAcquire方法,它是不允许重入的,而ReentrantLock是允许重入的:

1)lock方法一旦获取了独占锁,表示当前线程正在执行任务中;
2)如果正在执行任务,则不应该中断线程;
3)如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;
4)线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;
5)之所以设置为不可重入,是因为我们不希望任务在调用像setCorePoolSize这样的线程池控制方法时重新获取锁。如果使用ReentrantLock,它是可重入的,这样如果在任务中调用了如setCorePoolSize这类线程池控制的方法,会中断正在运行的线程。

所以,Worker继承自AQS(AbstractQueuedSynchronizer类),用于判断线程是否空闲以及是否可以被中断。

此外,在构造方法中执行了setState(-1);,把state变量设置为-1,为什么这么做呢?是因为AQS中默认的state是0,如果刚创建了一个Worker对象,还没有执行任务时,这时就不应该被中断。tryAcquire方法是根据state是否是0来判断的,所以,setState(-1);将state设置为-1是为了禁止在执行任务前对线程进行中断。正因为如此,在runWorker方法中会先调用Worker对象的unlock方法将state设置为0.

4)ThreadPoolExecutor类#runWorker方法【这里有涉及到一个概念,执行优先级: 核心线程>非核心线程>队列】

代码展示

汇总说明

总结一下runWorker方法的执行过程:

1)while循环不断地通过getTask()方法获取任务;
2)getTask()方法从阻塞队列中取任务;
3)如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态;调用task.run()执行任务;
4)如果task为null则跳出循环,执行processWorkerExit()方法;

5)ThreadPoolExecutor类#getTask方法

代码展示

汇总说明

运行状态下(这种情况下会把超出核心线程数的部分进入回收,也有一定概率回收核心线程):
情况1:当有非核心线程数的时候,timed为true,导致调用poll方法,这时候如果没有任务且超时,timedOut变为true,第二次进入自旋,timed还是true,进入判断会走compareAndDecrementWorkerCount,线程数减一,并返回null。(这种情况存在极端情况就是,全部线程走到同一逻辑去减,导致全部线程数都被减完了【即时有着wc > 1的判断,因为多线程并发情况,你懂得】)

情况2:没有非核心线程数,timed为false,导致调用take方法,线程一致阻塞直至,拿到任务。(这时候不存在减少线程)

非运行状态下(这种情况下是线程都会进入回收):
情况3:如果线程状态是STOP,TIDYING,TERMINATED,那么调用decrementWorkerCount,线程数减一,返回null。
情况4:如果线程状态是SHUTDOWN,队列不为空,则继续任务,如果队列为空,那么调用decrementWorkerCount,线程数减一,返回null。

所以,综上所述,非核心线程和核心线程其实都存在被回收的概率。

6)ThreadPoolExecutor类#processWorkerExit方法

代码展示

代码说明

通过设置allowCoreThreadTimeOut参数,我们可以选择核心线程的回收,在不用的时候保留一个worker。(这种更适用于某时间段高并发,其余时间段工作量不足的情况)

7)ThreadPoolExecutor类#tryTerminate方法

8)ThreadPoolExecutor类#shutdown方法

9)ThreadPoolExecutor类#interruptIdleWorkers方法

10)ThreadPoolExecutor类#hutdownNow方法

11)问题思考

1.在runWorker方法中,执行任务时对Worker对象w进行了lock操作,为什么要在执行任务的时候对每个工作线程都加锁呢?

(1)在getTask方法中,如果这时线程池的状态是SHUTDOWN并且workQueue为空,那么就应该返回null来结束这个工作线程,而使线程池进入SHUTDOWN状态需要调用shutdown方法;

(2)shutdown方法会调用interruptIdleWorkers来中断空闲的线程,interruptIdleWorkers持有mainLock,会遍历workers来逐个判断工作线程是否空闲。但getTask方法中没有mainLock;

(3)在getTask中,如果判断当前线程池状态是RUNNING,并且阻塞队列为空,那么会调用workQueue.take()进行阻塞;

(4)如果在判断当前线程池状态是RUNNING后,这时调用了shutdown方法把状态改为了SHUTDOWN,这时如果不进行中断,那么当前的工作线程在调用了workQueue.take()后会一直阻塞而不会被销毁,因为在SHUTDOWN状态下不允许再有新的任务添加到workQueue中,这样一来线程池永远都关闭不了;

(5)由上可知,shutdown方法与getTask方法(从队列中获取任务时)存在竞态条件;

(6)解决这一问题就需要用到线程的中断,也就是为什么要用interruptIdleWorkers方法。在调用workQueue.take()时,如果发现当前线程在执行之前或者执行期间是中断状态,则会抛出InterruptedException,解除阻塞的状态;

(7)但是要中断工作线程,还要判断工作线程是否是空闲的,如果工作线程正在处理任务,就不应该发生中断;

(8)所以Worker继承自AQS,在工作线程处理任务时会进行lock,interruptIdleWorkers在进行中断时会使用tryLock来判断该工作线程是否正在处理任务,如果tryLock返回true,说明该工作线程当前未执行任务,这时才可以被中断。

【6】额外拓展

Original: https://www.cnblogs.com/chafry/p/16730209.html
Author: 忧愁的chafry
Title: 线程池底层原理详解与源码分析

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/622647/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 从硬件缓存入门到并发编程三要素详解 Java中 volatile 、final 等关键字解析、单例模式案例

    举个简单的例子,比如下面的这段代码: i = i + 1; 当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数…

    Java 2023年6月7日
    050
  • day03-3私聊功能

    多用户即时通讯系统03 4.编码实现02 4.4功能实现-私聊功能实现 4.4.1思路分析 客户端 – 发送者: 用户在控制台输入信息,客户端接收内容 将消息构建成Me…

    Java 2023年6月15日
    059
  • 双色球系统开发

    Java对彩票双色球系统开发的简单实现 双色球系统 案例: 中奖条件及奖金表 代码及解释 main方法代码: public static void main(String[] ar…

    Java 2023年6月6日
    081
  • mybatis-generator生成domain和mapper,以及example的使用

    一:生成 1.效果 其中,domain,mapper等文件夹与文件都是插件生成 2.pom

    Java 2023年5月30日
    089
  • 面试题:请写出线程同步相关的方法,以银行账号存储款为例

    一.该面试题主要考察多线程中的synchronized或者Lock的使用 * 线程同步 :使用同步方法,实现线程同步 * 同步synchronized方法的对象监视锁为this,当…

    Java 2023年5月30日
    084
  • Java应用层数据链路追踪(附优雅打印日志姿势)

    我是3y,一年 CRUD经验用十年的 markdown程序员👨🏻‍💻常年被誉为优质八股文选手 今天来聊些大家都用得上的东西: 数据链路追踪。之前引入了系统的监控来快速定位应用操作系…

    Java 2023年6月9日
    059
  • Maven Archetype 多 Module 自定义代码脚手架

    大部分公司都会有一个通用的模板项目,帮助你快速创建一个项目。通常,这个项目需要集成一些公司内部的中间件、单元测试、标准的代码格式、通用的代码分层等等。 今天,就利用 Maven 的…

    Java 2023年6月13日
    098
  • springboot分析——自定义启动类

    在实际开发过程中,如果有一些公共功能,我们可以单独封装,然后配置成starter启动类,其他的项目需要使用时,主要 只要依赖开启就可以了。下面我们自定义一个自动配置启动类。 一:自…

    Java 2023年5月30日
    068
  • java 获取类路径下的资源文件

    一、问题 在用freemarker生成word文档的时候,在本地可以成功获取到类路径下的资源文件。但是打了jar包放在linux系统下启动,无法获取到该文件,导致生成的word文档…

    Java 2023年6月16日
    083
  • jdk1.8使用枚举类

    package com.mq; import java.util.Arrays; import java.util.HashMap; import java.util.Map; p…

    Java 2023年5月30日
    087
  • 人生苦短,我用python之三

    HTTP协议及Requests库的方法 requests库的主要方法:requests.request()构造一个请求 requests.get()获取HTML网页的主要方法,对应…

    Java 2023年6月7日
    075
  • tar、gzip、zip、jar是什么,怎么查看?

    原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。 如果你是后端程序员,我想你一定见过 *.tar.gz、 *.zip、 *.jar后缀的文件吧,这些都…

    Java 2023年6月7日
    092
  • SpringBoot:SpringBoot项目启动后立即执行函数的两种方式

    前言 有时启动SpringBoot项目后需要自运行函数来满足一些项目需求,下面介绍三种方式以此实现。 一、定义实体类实现ApplicationRunner接口 @Component…

    Java 2023年5月30日
    071
  • Java学到什么程度能找到一份还不错的工作

    我的读者里有很多 Java 新人,新人是指正在学 Java 的、以及工作时间不长的年轻人,他们经常问我一个问题: Java 学到什么程度才能找到一份还不错的工作? 今天我就从我自己…

    Java 2023年6月7日
    082
  • 单点登录SSO(Single Sign On)

    token表示按照一定的规则(通用的、官方的,如JWT)生成的字符串(可以包含用户的信息) jwt头部信息 有效载荷,包含用户主体信息 签名哈希,防伪标志 在任意一个模块进行登录,…

    Java 2023年6月13日
    069
  • MarkDown学习

    二级标题 … +空格+标题的名字(可以写到六级标题,写几个#号就代表是几级标题) 在文字两边都加两个星号 在文字两边都加一个星号 斜体加粗 在文字两边各加三个星号 删除…

    Java 2023年6月7日
    0104
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球