线程池第一讲:基本工作原理和源码解析

33 阅读14分钟

思维导图:

画板

一、线程

1、什么是线程

其实就是进程中一块程序的运行态,和其他线程一起共享进程内存。

下面这段理解是看别人的总结:

计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻的在运行。单个CPU一次只能运行一个任务。进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

1.2、线程生命周期

这个图后面我改一下:超时等待态 与 运行态的交互中,触发时机/函数上下写反了.

如图,线程的生命周期可以表述为:

初始态

创建一个Thread 对象,但是还没有调用start() 启动线程

运行态

在Java 语言中,运行态分为 运行态和就绪态

  • 就绪态
    • 代表线程已经准备好了所有资源,只需要CPU分配执行权或者时间片即可
  • 运行态
    • 获取CPU执行权,正在执行的线程
    • 由于同一个CPU一个时刻只能执行一个线程,因此每个CPU一个时刻只有一条运行态的线程。
阻塞态
  • 当一个正在执行的线程请求某一个资源失败的时候,就会进入阻塞态
  • 在java中,阻塞态指请求锁失败时进入的状态
  • 由一个阻塞队列存放
  • 处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行
等待态
  • 当前线程中调用wait、join、park函数时,当前线程就会进入等待态。
  • 也有一个等待队列存放所有等待态的线程。
  • 线程处于等待态表示它需要等待其他线程的指示才能继续运行。
  • 进入等待态的线程会释放CPU执行权,并释放资源(如:锁)
超时等待态
  • 当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态;
  • 它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其他线程唤醒;
  • 进入该状态后释放CPU执行权 和 占有的资源。
  • 与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。

终止态

线程结束后的状态。对一个线程来说,执行完run方法,线程就会进入终止态。

2、生产者消费者模式

如图,生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

3、池化技术

1、为啥要用池化技术

首先,什么是池化技术?将对象放入池子中,使用时从池子获取,不用时放入池子管理。通过优化资源分配的效率,达到性能的调优。

为什么需要一个池子去管理对象呢? 因为创建对象或者链接都是由开销的,就像公司,一名新员工,培训,上手,干活,熟练等,干完活了就辞退,成本很高。

2、常见的池化技术

比如连接池,线程池等。连接池: mysql、redis、netty、http连接池等.

二、线程池

1、主要作用

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性

2、基本用法

2.1、核心类

其实核心类很简单,只有一个类ThreadPoolExecutor, 只要理解了这个类其实就了解了线程池.

ThreadPoolExecutor核心包括

  1. executor()方法 用于提交任务
  2. Worker 线程池消费任务的线程
  3. BlockingQueue workQueue 线程池的工作队列
  4. HashSet workers 工作线程的集合.

2.2、七个参数

最开始学习这个类的时候,都是从这七个参数开始学习的,这七个参数也决定了我们创建的是一个怎样(提供什么样的能力)的线程池。七个参数语义如下:

corePoolSize 线程池核心线程大小

一个最小的线程数量,即使这些线程处于空闲状态,他们也不会被销毁。除非设置了allowCoreThreadTimeOut

maximumPoolSize 线程池最大线程数量

当核心线程数等于corePoolSize 并且阻塞队列满了之后,会创建非核心线程,核心线程数 + 非核心线程数不大于maximumPoolSize.

keepAliveTime 多余的空闲线程存活时间

当线程空闲时间达到keepAliveTime 值时,多余的线程会被销毁直到只剩下corePoolSize 个线程为止。

默认情况下: 只有当线程池中的线程数大于corePoolSize 时,keepAliveTime 才会起作用,直到线程中的线程数不大于 corePoolSize.

TimeUnit 空闲线程存活时间单位

keepAliveTime 的计量单位

workQueue 工作队列

核心线程数是corePoolSize时,任务会被提交到工作队列

threadFactory 线程工厂

创建一个线程工厂用来创建线程,可以用来设定线程名、是否为daemon线程等

handler 拒绝策略
  • AbortPolicy: 丢弃任务并抛出 RejectedExecutionException异常. (默认这种)
  • DiscardPolicy: 丢弃任务,但是不抛出异常
  • DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务 (重复此过程)。也即是当任务被拒绝添加时,会丢弃队列中最旧的任务,再把这个新任务加入到队列尾部,等待执行.
  • CallerRunsPolicy: 谁调用,谁处理。由调用线程(即提交任务给线程池的线程)处理该任务,如果线程池已经被shutdown则直接丢弃.

2.3、基本原理

3.1、整体流程

画板

如上图,线程池的执行流程如下:

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传递进来的。不过,这个时候就算里面有任务,线程池也不会立即执行他们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    1. 如果正在运行的线程数量小于corePoolSize, 那么马上创建线程运行这个任务。
    2. 如果正在运行的线程数量大于或者小于corePoolSize, 则将这个任务放入任务队列中。
    3. 如果这个时候队列满了,并且正在运行的线程数量小于 maximumPoolSize, 那么还是要创建非核心线程立即运行这个任务。
    4. 如果队列满了,并且正在运行的线程数量大于或者等于 maximumPoolSize, 那么线程池会根据拒绝策略进行处理
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做,超过一定的时间 (keepAliveTime) 时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就会被停止。线程池的所有任务完成后,它最终会收缩到corePoolSize 的大小.
3.2、worker是什么?

worker 其实就是任务的具体执行的对象。每个任务提交之后,线程池都会把它交给worker执行。同时,所有的worker对象会放入一个HashSet, 这也是线程池最底层的存储单元。

Worker详解:

addWorker中

Worker也是一个 Runnable, 同时将一个thread作为成员变量,这个thread就是要开启的线程。

在构造方法中,创建Worker的同时也创建了一个thread对象,同时将worker对象作为参数传递给thread。这样当thread的start()方法调用时,运行的实际上是Worker的run()方法,run()方法执行worker对象的runWorker方法。

画板

我们重点看一下runWorker() 方法,红框的部分就是整个worker的核心,也就是整个线程池的核心方法。

  1. runWorker会首先取当前worker对象持有的firstTask, 也就是创建worker的时候携带的runnable, 取到firstTask之后,会把firstTask置空,防止任务重复执行。
  2. while循环会不断的取task,如果当前task不为空,就执行firstTask。如果firstTask已经执行完了,就从queue中getTask执行.
  3. while方法内部会执行task.run() 方法,也就是execute传入的runnable的run()方法.
3.3、阻塞队列是干啥的

当worker数量达到 corePoolSize 之后,按照我们之前说的线程池的执行流程。

execute 新提交的runnable 显然没办法通过firstTask执行,这个时候,就需要把任务放入阻塞队列中.

private final BlockingQueue workQueue;

任务提交到阻塞队列:

  1. 如果当前运行的 线程少于 corePoolSize,此时尝试 把任务赋于 给 一个worker。调用addWorker会自动检查runState 和 workerCount,这样可以防止误报警 在不应该执行线程时返回false
  2. 如果一个任务可以成功排队,此时我们仍然需要检查是否应该加入一个线程(因为自上次检查以来先有的已死亡)或者其他进入此方法后池子关闭。所以我们重新检查状态,必须要时回滚队列 stopeed,或者在没有线程时启动一个新线程。
  3. 如果我们不能排队任务,比如队列满了,此时尝试addWorker 。如果失败了,比如超过最大线程数 | 被关闭了,此时会拒绝该任务。

在execute 方法的内部,会通过BlockingQueue的 offer() 方法** 非阻塞**的加入到队列中,注意这里使用的是 offer(runnable) 而不用带超时时间的方法(当满了会阻塞指定时间)。

这里的offer 是 当线程池满了,直接返回false, 否则true

CTL:
  1. 记录 workerCount
  2. 记录 线程池 所有worker的 runState
/**
 * The main pool control state, ctl, is an atomic integer packing
 * two conceptual fields
 *   workerCount, indicating the effective number of threads
 *   runState,    indicating whether running, shutting down etc
 *
 * In order to pack them into one int, we limit workerCount to
 * (2^29)-1 (about 500 million) threads rather than (2^31)-1 (2
 * billion) otherwise representable. If this is ever an issue in
 * the future, the variable can be changed to be an AtomicLong,
 * and the shift/mask constants below adjusted. But until the need
 * arises, this code is a bit faster and simpler using an int.
 *
 * The workerCount is the number of workers that have been
 * permitted to start and not permitted to stop.  The value may be
 * transiently different from the actual number of live threads,
 * for example when a ThreadFactory fails to create a thread when
 * asked, and when exiting threads are still performing
 * bookkeeping before terminating. The user-visible pool size is
 * reported as the current size of the workers set.
 *
 * The runState provides the main lifecycle control, taking on values:
 *
 *   RUNNING:  Accept new tasks and process queued tasks
 *   SHUTDOWN: Don't accept new tasks, but process queued tasks
 *   STOP:     Don't accept new tasks, don't process queued tasks,
 *             and interrupt in-progress tasks
 *   TIDYING:  All tasks have terminated, workerCount is zero,
 *             the thread transitioning to state TIDYING
 *             will run the terminated() hook method
 *   TERMINATED: terminated() has completed
 *
 * The numerical order among these values matters, to allow
 * ordered comparisons. The runState monotonically increases over
 * time, but need not hit each state. The transitions are:
 *
 * 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
 *
 * Threads waiting in awaitTermination() will return when the
 * state reaches TERMINATED.
 *
 * Detecting the transition from SHUTDOWN to TIDYING is less
 * straightforward than you'd like because the queue may become
 * empty after non-empty and vice versa during SHUTDOWN state, but
 * we can only terminate if, after seeing that it is empty, we see
 * that workerCount is 0 (which sometimes entails a recheck -- see
 * below).
 */

*主池控制状态ctl是一个原子整数封装
*两个概念域
* workerCount,表示有效线程数
* runState,表示是否运行,正在关闭等
为了将它们打包到一个int中,我们将workerCount限制为
*(2^29)-1(大约5亿个)线程,而不是(2^31)-1 (2
*十亿)否则是可代表的。如果这是一个问题
*未来,该变量可以更改为AtomicLong;
*和下面调整的移位/掩码常数。但在需要之前
*出现时,这段代码使用int会更快更简单。
* workerCount是工作人员的数量
允许启动,不允许停止。值可以是
*暂时不同于实际的活动线程数;
例如当ThreadFactory创建线程失败时
当退出的线程仍在执行时问
*终止前的簿记。用户可见的池大小为
*报告为当前工人集的大小。
* runState提供了主要的生命周期控制,接受值:
* RUNNING:接受新任务并处理队列任务
* SHUTDOWN:不接受新任务,但处理排队的任务
* STOP:不接受新任务,不处理队列任务,
*并中断正在进行的任务
* TIDYING:所有任务已终止,workerCount为零,
*线程过渡到状态整理
*将运行terminated()钩子方法
* TERMINATED()已完成
*这些值之间的数字顺序很重要,以允许
*有序比较。runState单调递增
*时间,但不需要击中每个状态。过渡是:
* running -> shutdown
*调用shutdown()

*(运行或关机)->停止
*调用shutdownNow()
*关机->整理
当队列和池都为空时
stop ->整理
*当池是空的
* tidying ->已终止

*当terminate()钩子方法完成时
在awaitTermination()中等待的线程将返回
*状态达到终止。
*检测从关机到整理的过渡更少
*比你想的简单,因为队列可能会变得
*非空后为空,反之亦然,在SHUTDOWN状态,但是
只有当我们看到它是空的,我们才可以终止
* workerCount为0(有时需要重新检查)
*以下)。

任务在阻塞队列的消费

其实这部分就是 getTask() 方法获取任务.

3.4 线程池的线程死了吗?

有个问题,我们的worker其实也是一个runnable,虽然构造的时候创建了线程,把worker作为了入参,一旦run方法执行完毕,worker的生命也就要终结了,那worker到底会死吗?

首先,我们回忆,在runWorker() 方法中,有while 循环不断从队列中getTask()执行。
但是如果队列中没有数据了呢?这个时候线程池如何处理?

  • 首先,在getTask()中, 会判断 timed 的true和 false
    • 如果允许核心线程池超时或者当前的线程数大于核心线程数,timed就是 true.
  • timed
    • true: 使用队列提供的poll()方法 非阻塞的移除.
    • false: 如果队列为空,就阻塞等待队列有数据.

poll如果队列为null, 就会返回为null. getTask() 就会为null 出循环

但是如果是false, 这个时候一直再阻塞等待队列有数据,这样run方法会一直执行,worker也不会"死"了。

前面也说了,getTask 会有为null 现象. 这个时候就会执行到finally 中的 processWorkerExit方法.

这个方法主要做了以下几个事情:

  • 减workerCount(减一)
  • 将当前worker 完成的任务数加到总任务数中,并从worker集合中移除当前的worker
  • 尝试终止线程.
  • 如果线程池状态是running 或者 shutdown 就尝试增加一个 新的worker。allowCoreThreadTimeOut默认为false,所以会保留corePoolSize的线程数,既然之前的worker已经执行完run方法了,生命已经走到尽头了,这个时候,只需要重新addWorker, 重新创建一个worker, 就可以保证核心线程数的动态平衡.

三、结束:

以上就是线程池的全部内容了,要想使用好线程池,就需要了解其原理、运行机制、并且在工作中多思考,设计层面,可观测,安全的设计高性能线程池。