线程池的基本概念

线程池,是一种线程的使用模式,它为了降低线程使用中频繁的创建和销毁所带来的资源消耗与代价。

通过创建一定数量的线程,让他们时刻准备就绪等待新任务的到达,而任务执行结束之后再重新回来继续待命。

这就是线程池最核心的设计思路,「复用线程,平摊线程的创建与销毁的开销代价」。

相比于来一个任务创建一个线程的方式,使用线程池的优势体现在如下几点:

  1. 避免了线程的重复创建与开销带来的资源消耗代价
  2. 提升了任务响应速度,任务来了直接选一个线程执行而无需等待线程的创建
  3. 线程的统一分配和管理,也方便统一的监控和调优

线程池的实现天生就实现了异步任务接口,允许你提交多个任务到线程池,线程池负责选用线程执行任务调度。

异步任务在上一篇文章中已经做过一点铺垫介绍,那么本篇就在前一篇的基础上深入的去探讨一下异步任务与线程池的相关内容。

基本介绍

在正式介绍线程池相关概念之前,我们先看一张线程池相关接口的类图结构,网上盗来的,但画的还是很全面的。

右上角的几个接口可以先不看,等我们介绍到组合任务的时候会继续说的,我们看左边,Executor、ExecutorService 以及 AbstractExecutorService 都是我们熟悉的,它们抽象了任务执行者的基本模型。

ThreadPoolExecutor 是对线程池概念的抽象,它天生实现了任务执行的相关接口,也就是说,线程池也是一个任务的执行者,允许你向其中提交多个任务,线程池将负责分配线程与调度任务。

至于 Schedule 线程池,它是扩展了基础的线程池实现,提供「计划调度」能力,定时调度任务,延时执行等。

线程池基本原理

ThreadPoolExecutor 的创建并不复杂,直接 new 就好,只不过构造函数有好久个重载,我们直接看最底层的那个,也就是参数最多的那个。

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

创建一个线程池需要传这么多参数?是不是觉得有点丧心病狂?

不要担心,我说了,这是最复杂的一个构造函数重载,需要传入最全面的构造参数。而你日常使用时,当然可以使用 ThreadPoolExecutor 中的其他较为简便的构造函数,只不过有些你没传的参数将配置为默认值而已。

下面我们将从这些参数的含义出发,看看线程池 ThreadPoolExecutor 具备一个怎样的构成结构。

1、线程池容量问题

构造函数中有这么几个参数是用于配置线程池中线程容量与生命周期的:

  • corePoolSize
  • maximumPoolSize
  • keepAliveTime

corePoolSize 指定了线程池中的核心线程的个数,核心线程就是永远不会被销毁的线程,一旦被创建出来就将永远存活在线程池之中。

maximumPoolSize 指定了线程池能够创建的最大线程数量。

keepAliveTime 是用于控制非核心线程最长空闲等待时间,如果一个非核心线程处理完任务后回到线程池待命,超过这个指定时长依然没有新任务的分配将导致线程被销毁。

2、任务阻塞问题

ThreadPoolExecutor 中有这么一个字段:

private final BlockingQueue

这个队列的作用很明显,就是当线程池中的线程不够用的时候,让任务排队,等待有线程空闲再来取任务去执行。

3、线程工厂

线程工厂 ThreadFactory 中只定义了一个方法 newThread,子类实现它并按照自己的需求创建一个线程返回。

例如 DefaultThreadFactory 实现的该方法将创建一个线程,名称格式: pool-<线程池编号>-thread-<线程编号>,设置线程的优先级为标准优先级,非守护线程等。

4、任务拒绝策略

构造函数中还有一个参数 handle 是必须传的,它将为 ThreadPoolExecutor 中的同名字段赋值。

private volatile RejectedExecutionHandler handler;

RejectedExecutionHandler 中定义了一个 rejectedExecution 用于描述一种任务拒绝策略。那么哪种情况下才会触发该方法的调用呢?

当线程池中的所有线程全部分配出去工作了,并且任务阻塞队列也阻塞满了,那么此时新提交的任务将触发任务拒绝策略

而拒绝策略主要有以下四个子类实现,而它们都是定义在 ThreadPoolExecutor 的内部类,我们看一看都是哪四种策略:

  • AbortPolicy
  • CallerRunsPolicy
  • DiscardOldestPolicy
  • DiscardPolicy

AbortPolicy 是默认的拒绝策略,他的实现就是直接抛出 RejectedExecutionException 异常。

CallerRunsPolicy 暂停当前提交任务的线程返回,自己去执行自己提交过来的任务。

DiscardOldestPolicy 策略将从阻塞任务队列对头移除一个任务并将自己排到队列尾部等待调度执行。

DiscardPolicy 是一种佛系策略,方法体的实现为空,什么也不做,也即忽略当前任务的提交。

这样,我们零零散散的对线程池的内部有了一个基本的认识,下面我们要把这些都串起来,看一看源码。从一个任务的提交,到分配到线程执行任务,一整个过程的相关逻辑做一个探究。

看一看源码

先来看一看任务的提交方法,submit

之前的文章我们也说过,这个 submit 方法有四个重载,分别允许你传入不同类型的任务,Runnable 或是 Callable。我们这里就以前者为例。

这个 RunnableFuture 类型我们之前说过,他只不过是同时继承了 Runnable 和 Future 接口,象征性的描述了「这是一个可监控的任务」。

然后你会发现,整个 submit 的核心逻辑在 execute 方法里面,也就是说 execute 方法才是真正向线程池提交任务的方法。我们重点看一看这个 execute 方法。

先看看 ThreadPoolExecutor 中定义几个重要的字段:

ctl 是一个原子变量,它用了一个 32 位的整型描述了两个重要信息。当前线程池运行状态(runState)和当前线程池中有效的线程个数(workCount)。

runState 占用高 3 比特位,workCount 占用低 29 比特位。

接着我们来看 execute 方法的实现:

红框部分:

如果当前线程池中的实际工作线程数还未达到配置的核心线程数量,那么将调用 addWorker 为当前任务创建一个新线程并启动执行。

addWorker 方法代码还是有点多的,这里就截图出来进行分析了,因为并不难,我们总结下该方法的逻辑:

  1. 死循环中判断线程池状态是否正常,如果不正常被关闭了等,将直接返回 false
  2. 如果正常则 CAS 尝试为 workerCount 增加一,并创建一个新的线程调用 start 方法执行任务。

不知道你留意到 addWorker 方法的第二个参数了没有,这个参数用于指定线程池的上界。

如果传的是 true,则说明使用 corePoolSize 作为上界,也就是此次为任务分配线程如果线程池中所有的工作线程数达到这个 corePoolSize 则将拒绝分配并返回添加失败。

如果传的是 false,则使用 maximumPoolSize 作为上界,道理是一样的。

蓝框部分:

从红框出来,你可以认为任务分配线程失败了,大概率是所有正常工作的线程数达到核心线程数量了。这部分做的事情就是:

  1. 如果线程池状态正常,就尝试将当前任务添加到任务阻塞队列上。
  2. 再一次检查线程池状态,如果异常了,将撤回刚才添加的任务并根据我们设定的拒绝策略予以拒绝。
  3. 如果发现线程池自上次检查后,所哟线程全部死亡,那么将创建一个空闲线程,适当的时候他会去从任务队列取我们刚刚添加的任务的

黄框部分:

到达黄色部分必然说明线程池状态异常或是队列添加失败,大概率是因为队列满了无法再添加了。

此时再次调用 addWorker 方法,不过这次传入 false,意思是,我知道所有的核心线程都在忙并且任务队列也排满了,那么你就额外创建一个非核心线程来执行我的任务吧。

如果失败了,执行拒绝策略。

我们总结一下任务的提交到分配线程,甚至阻塞到任务队列这一系列过程:

一个任务过来,如果线程池中的线程数不足我们配置的核心线程数,那么会尝试创建新线程来执行任务,否则会优先把任务往阻塞队列上添加

如果阻塞队列上满员了,那么说明当前线程池中核心线程工作量有点大,将开始创建非核心线程共同执行任务,直到达到上限或是阻塞队列不再满员。

到这里呢,我们对于任务的提交与线程分配已经有了一个基本的认识了,相信你也一定好奇当一个线程的任务执行结束之后,他是如何去取下一个任务的。

这部分我们也来分析分析

线程池的内部定义了一个 Worker 内部类,这个类有两个字段,一个用于保存当前的任务,一个用于保存用于执行该任务的线程。

addWorker 中会调用线程的 start 方法,进而会执行 Worker 实例的 run 方法,这个 run 方法是这样的:

public void run() {
    runWorker(this);
}

runWorker 很长,就不截出来一点点分析了,我总结下他的实现逻辑:

  1. 如果自己内部的任务是空,则尝试从阻塞队列上获取一个任务
  2. 执行任务
  3. 循环的执行 1和2 两个步骤,直到阻塞队列中没有任务可获取
  4. 调用 processWorkerExit 方法移除当前线程在线程池中的引用,也就相当于销毁了一个线程,因为不久后会被 GC 回收

但是这里有一个细节和大家说一下,第一个步骤从任务队列中取一个任务调用的是 getTask 方法。

这个方法设定了一个逻辑,如果线程池中正在工作的线程数大于设定的核心线程数,也就是说线程池中存在非核心线程,那么当前线程获取任务时,如果超过指定时长依然没有获取,就将返回跳过循环执行我们 runWorker 的第四个步骤,移除对该线程的引用。

反之,如果此时有效工作线程数少于规定的核心线程数,则认定当前线程是一个核心线程,于是对于获取任务失败的处理是「阻塞到条件队列上,等待其他线程唤醒」。

什么时候唤醒也很容易想到了,就是当任务队列有新任务添加时,会唤醒所有的核心线程,他们会去队列上取任务,没抢到的依然回去阻塞。

至此,线程池相关的内容介绍完毕,有些方法的实现我只是总结了大概的逻辑,具体的尤待你们自己去探究,有问题也欢迎你和我讨论。

关注公众不迷路,一个爱分享的程序员。

公众号回复「1024」加作者微信一起探讨学习!

每篇文章用到的所有案例代码素材都会上传我个人 github

https://github.com/SingleYam/overview_java

欢迎来踩!

原文地址:https://www.cnblogs.com/yangming1996/p/10287109.html

时间: 2024-10-18 13:39:57

线程池的基本概念的相关文章

计算机程序的思维逻辑 (78) - 线程池

上节,我们初步探讨了Java并发包中的任务执行服务,实际中,任务执行服务的主要实现机制是线程池,本节,我们就来探讨线程池. 基本概念 线程池,顾名思义,就是一个线程的池子,里面有若干线程,它们的目的就是执行提交给线程池的任务,执行完一个任务后不会退出,而是继续等待或执行新任务.线程池主要由两个概念组成,一个是任务队列,另一个是工作者线程,工作者线程主体就是一个循环,循环从队列中接受任务并执行,任务队列保存待执行的任务. 线程池的概念类似于生活中的一些排队场景,比如在火车站排队购票.在医院排队挂号

线程池(C#)

转自:http://blog.sina.com.cn/s/blog_494305f30100ryw7.html 在这里你可以学到Microsoft研究CLR实现线程池的原理机制,从而更灵活的处理CLR在实际代码应中线程池的问题,下面我们来看看吧. CLR教程之线程池的产生 当 CLR 初始化时,其线程池中不含有线程.当应用程序要创建线程来执行任务时,该应用程序应请求线程池线程来执行任务.线程池知道后将创建一个初始线程. 该新线程经历的初始化和其他线程一样:但是任务完成后,该线程不会自行销毁.相反

深入理解java线程池—ThreadPoolExecutor

几句闲扯:首先,我想说java的线程池真的是很绕,以前一直都感觉新建几个线程一直不退出到底是怎么实现的,也就有了后来学习ThreadPoolExecutor源码.学习源码的过程中,最恶心的其实就是几种状态的转换了,这也是ThreadPoolExecutor的核心.花了将近小一周才大致的弄明白ThreadPoolExecutor的机制,遂记录下来. 线程池有多重要##### 线程是一个程序员一定会涉及到的一个概念,但是线程的创建和切换都是代价比较大的.所以,我们有没有一个好的方案能做到线程的复用呢

26_多线程_第26天(Thread、线程创建、线程池)

今日内容介绍1.多线程2.线程池 01进程概念 A:进程概念 a:进程:进程指正在运行的程序.确切的来说,当一个程序进入内存运行, 即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能. 02线程的概念 A:线程的概念 a:线程:线程是进程中的一个执行单元(执行路径),负责当前进程中程序的执行, 一个进程中至少有一个线程.一个进程中是可以有多个线程的, 这个应用程序也可以称之为多线程程序. 简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程 03深入线程的概念 A:深

Python的定时器与线程池

定时器执行循环任务: 知识储备 Timer(interval, function, args=None, kwargs=None) interval ===> 时间间隔 单位为s function ===> 定制执行的函数 使用threading的 Timer 类 start() 为通用的开始执行方法 cancel ()为取消执行的方法 普通单次定时执行 from threading import Timer import time # 普通单次定时器 def handle(): print(

06:线程池

1:线程池原理-基本概念: 1:线程池管理器:用户管理线程池.包括创建线程池.销毁线程池,添加新任务等. 2:工作线程:工作线程就是线程池中实际工作的线程.没有任务时:处于等待状态,有任务时:可以循环的执行任务. 3:任务接口:每个任务都需要实现的接口.规范了任务的输入.输出等. 4:任务队列:任务太多时,超过了线程池处理能力.将待处理的任务放到等待队列中. 2:线程池接口和实现类: 1:接口:Executor:最上层的接口:定义了执行任务的方法:executor() 2:接口:Executor

Java 线程池概念、原理、简单实现

线程池的思想概述 我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结東了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间.那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?Java中可以通过线程池来达到这样的效果.下面们就来详细讲解一下Java的线程池. 线程池概念 线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省

线程池的概念 与Executors 类的应用

创建固定大小的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(3); 创建缓存线程池       ExecutorService threadPool = Executors.newCachedThreadPool(3); 创建单一线程池    ExecutorService threadPool = Executors.newSingleThreadExecutor(3);(线程死掉后重新生成新的线程) 线程池定时器 

线程池 概念理解

转 Java线程池解析 作者 whthomas 关注 2016.04.06 20:07* 字数 1427 阅读 340评论 0喜欢 8 Java的一大优势是能完成多线程任务,对线程的封装和调度非常好,那么它又是如何实现的呢? jdk的包下和线程相关类的类图. 屏幕快照 2016-04-06 下午2.01.16.png 从上面可以看出Java的线程池主的实现类主要有两个类ThreadPoolExecutor和ForkJoinPool. ForkJoinPool是Fork/Join框架下使用的一个线