多线程之线程池
线程池
1、线程
1.1、线程状态
线程是CPU调度资源的最小单位,线程模型分为KLT模型与ULT模型,JVM使用的KLT模型,Java线程与OS线程保持1:1的映射关系,也就是说有一个java线程也会在操作系统里有一个对应的线程。在ThreadLocal类中有一个枚举类State定义了线程的状态,如下所示:
NEW,新建
RUNNABLE,运行
BLOCKED,阻塞
WAITING,等待
TIMED_WAITING,超时等待
TERMINATED,终结
状态切换如下图所示:
1.2、创建线程方式
Runnable,Thread,Callable
// 实现Runnable接口的类将被Thread执行,表示一个基本的任务
public interface Runnable {
// run方法就是它所有的内容,就是实际执行的任务
public abstract void run();
}
// Callable同样是任务,与Runnable接口的区别在于它接收泛型,同时它执行任务后带有返回内容
public interface Callable<V> {
// 相对于run方法的带有返回值的call方法
V call() throws Exception;
}
2、线程池
“线程池”,顾名思义就是一个线程缓存,线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此Java中提供线程池对线程进行统一分配、 调优和监控和管理等等情况。
2.1、线程池介绍
在web开发中,服务器需要接受并处理请求,所以会为一个请求来分配一个线程来进行处理。如果每次请求都新创建一个线程的话实现起来非常简便,但是存在一个问题:
如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,如此一来会大大降低系统的效率。可能出现服务器在为每个请求创建新线程和销毁线程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多。
那么有没有一种办法使执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢?
这就是线程池的目的了。线程池为线程生命周期的开销和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。
2.2、什么时候使用线程池?
-
单个任务处理时间比较短
-
需要处理的任务数量很大
2.3、线程池优势
-
重用存在的线程,减少线程创建,消亡的开销,提高性能
-
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
3、框架体系
Executor接口是线程池框架中最基础的部分,定义了一个用于执行Runnable的execute方法。
那么接下来可以看一下,各个层级之间都做了一些什么事情:
Executor接口中规定了线程池中主要是为了提交一个Runnable类型的任务,也就是线程池来执行任务。这里也就说明了将线程和任务隔离开来
ExecutorService同样是个接口,但是它算是搭建出来了线程池的框架了,给线程池提供了管理线程的方法。
1,execute(Runnable command):执行Ruannable类型的任务;
2,submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future对象;
3,shutdown():在完成已提交的任务后封闭办事,不再接管新任务;
4,shutdownNow():停止所有正在履行的任务并封闭办事;
5,isTerminated():测试是否所有任务都履行完毕了;
6,isShutdown():测试是否该ExecutorService已被关闭;
AbstractExecutorService 是普通的线程池执行器,ScheduledExecutorService 是定时任务线程池。
4、线程池重要属性
4.1、ctl属性
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE;
private static final int CAPACITY = (1 << COUNT_BITS) ;
ctl 是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它包含两部分的信息: 线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),这里可以看到,使用了Integer类型来保存,高3位保存runState,低29位保存workerCount。COUNT_BITS 就是29,CAPACITY就是1左移29位减1(29个1),这个常量表示workerCount的上限值,大约是5亿
4.2、ctl属性相关方法
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
runStateOf:runStateOf方法取出高3位的值,表示当前线程池的运行状态;
workerCountOf:workerCountOf方法取出低29位的值,表示当前活动的线程数
ctlOf:获取运行状态和活动线程数的值;
5、线程池的生命状态
线程池中存在着五种生命状态,对应下面所示:
RUNNING = ‐1 << COUNT_BITS; //高3位为111
SHUTDOWN = 0 << COUNT_BITS; //高3位为000
STOP = 1 << COUNT_BITS; //高3位为001
TIDYING = 2 << COUNT_BITS; //高3位为010
TERMINATED = 3 << COUNT_BITS; //高3位为011
5.1、RUNNING
(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(02) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0
5.2、SHUTDOWN
(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
5.3、STOP
(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
5.4、TIDYING
(1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理; 可以通过重载terminated()函数来实现。
(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5.5、TERMINATED
(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING - > TERMINATED。
进入TERMINATED的条件如下:
-
线程池不是RUNNING状态;
-
线程池状态不是TIDYING状态或TERMINATED状态;
-
如果线程池状态是SHUTDOWN并且workerQueue为空;
-
workerCount为0;
-
设置TIDYING状态成功。
6、ThreadPoolExecutor
6.1、线程池的创建
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
6.2、线程池参数
线程池中的每个构造函数的方法和意义对应下面的含义:
参数名称 | 概述 |
---|---|
corePoolSize | 核心线程数量 |
maximumPoolSize | 线程池中最大能够容纳的线程数量 |
keepAliveTime | 通常来说,是非核心线程超时多久之后从线程池中移除 |
unit | 超时时间单位 |
workQueue | 阻塞队列 |
threadFactory | 线程工厂,用来生产线程的工厂。它是ThreadFactory类型的变量,用来创建新线程。默认使用 Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程 |
handler | 拒绝策略。当核心和非核心线程都在工作,队列也满了,再次新来的任务将会被拒绝 |
6.3、阻塞队列分类
用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
ArrayBlockingQueue | 基于数组结构的有界阻塞队列,按FIFO排序任务 |
---|---|
LinkedBlockingQuene | 基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene |
SynchronousQuene | 一个不存储元素的阻塞队列,每个插入操作必须等到 另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene |
priorityBlockingQuene | 具有优先级的无界阻塞队列 |
6.4、拒绝策略分类
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略
AbortPolicy | 直接抛出异常,默认策略 |
---|---|
CallerRunsPolicy | 用调用者所在的线程来执行任务 |
DiscardOldestPolicy | 丢弃阻塞队列中靠最前的任务,并执行当前任务 |
DiscardPolicy | 直接丢弃任务 |
上面的4种策略都是ThreadPoolExecutor的内部类。 当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
6.5、给线程池提交任务
第一种方式:
public void execute() //提交任务无返回值
第二种方式:
public Future<?> submit() //任务执行完成后有返回值
但是看submit方法的时候,可以看到仍然调用的是execute方法,所以重点就是研究这里的execute方法
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
6.6、监控线程池信息
能够知道任务和线程。得知了这些信息可以动态的来进行扩展!
public long getTaskCount() //线程池已执行与未执行的任务总数
public long getCompletedTaskCount() //已完成的任务数
public int getPoolSize() //线程池当前的线程数
public int getActiveCount() //线程池中正在执行任务的线程数量
因为可以在线程池的构造中看到,除了阻塞队列不是使用volatile关键字修饰的,其他的都是可以来做到的。
那么我们可以在线程池动态运行的过程中,可以来修改其中的一些参数信息!达到动态的效果。
6.7、线程池执行原理图
前提条件:当前的线程池的生命状态一直处于RUNNING状态
7、源码分析
问题一:非核心线程执行任务条件
线程提交任务的时候,如果核心线程还没有达到最大值的情况下,每次新来一个任务的时候,都将会创建一个新的核心线程来处理任务。
即使是之前的核心线程处理完了任务,也还是新建任务来进行执行,直到将核心线程数创建慢为止。
核心线程数满了之后且都在处理任务,那么再次新来的任务将没有核心线程来进行处理了。那么将会进入到阻塞队列中去排队
核心线程处理完任务之后,回去阻塞队列中去获取得到对应的任务来进行运行。
7.1、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;
// 如果创建线程失败了,那么再次获取ctl的值,用来判断当前线程池的生命状态
c = ctl.get();
}
// 两个情况:活跃线程数大于核心线程数或者是创建worker失败(因为线程池生命状态变化)
// 如果线程池处于running状态(可能是上面创建worker失败,检查线程池状态),再向阻塞队列中添加任务
if (isRunning(c) && workQueue.offer(command)) {
// 再次检查
int recheck = ctl.get();
// 如果发现当前线程池不是运行状态,上面已经将任务添加到阻塞队列中,所以需要将任务进行移除,并将任务进行拒绝。
// 说明:线程处于shutdown状态的时候,不再接收新的任务但是会处理已经存在的任务
// 如果是STOP状态的,也会拒绝任务执行
if (!isRunning(recheck) && remove(command))
reject(command);
// 前提条件:线程池是运行状态且任务存在队列中,然而此时线程中没有活跃的线程,
// 检查到线程池中是没有线程的,所以在线程池中创建了一个线程,让线程从队列中来获取得到任务执行
// 如果没有活跃且核心,有可能是临时执行的
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 程序执行到这里,说明线程池不是处于运行状态 or 阻塞队列满了
// 1、如果不是处于运行状态,那么不会接收新的任务。(addWorker中的if判断中可以看到)
// 2、如果队列满了,会尝试创建非核心线程和第一个任务,如果创建失败,说明非核心线程都在工作,直接将新来的任务拒绝
else if (!addWorker(command, false))
reject(command);
}
上面的执行流程就是线程池的执行原理图!上面的执行流程图只是将代码进行了转换而已。
那么在这里用文字来进行叙述这里执行的流程:
前提条件:加入线程池的生命状态一直处于RUNNING状态:
1、如果当前线程池中的线程数量<核心线程数量,那么在线程池中创建线程,并将任务丢给新创建的线程来执行;
2、如果当前线程池中线程数量=核心线程数量&&当前线程池中的线程数量<最大线程数量,那么将会把任务放置到任务队列中;
3、如果任务队列已满且<线程池最大线程数,那么将会创建线程来执行新进来的任务;
4、如果当前活跃的线程数量已经=线程池最大线程数,那么将会被直接丢弃掉;
备注:
1、线程池中的线程执行完成之后,将会从任务队列中来获取得到任务来进行执行;
2、所谓的核心线程和非核心线程没有明显的区别,在地位上都是等价的,也没有特别的标识,本质上来说都是一样的;
3、addWorker(command, true)、addWorker(command, false)和addWorker(null, false)的区别:
方法参数 | 说明 |
---|---|
addWorker(command, true) | 说明当前线程池中活跃的线程数量没有达到核心线程数,创建核心线程并将第一个任务给当前线程 |
addWorker(command, false) | 当前任务队列满了,创建非核心线程数并将任务给当前线程 |
addWorker(null, false) | 在创建的时候没有直接赋予任务,而是在线程池中从任务队列中来进行获取 |
从上面的一系列的流程中,没有看到一个线程,但是却反复看到addWorker方法,也就是说创建任务交给这个方法,那么这个方法的底层应该是和线程和任务是有关系的。
7.2、addWorker方法
分为两步来进行执行:1、判断线程池状态;2、线程池状态满足接收任务的条件,那么任务执行;
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 注意这里的条件,如果线程池状态是非运行状态,那么直接返回返回FALSE,拒绝掉任务。这里对应的上面的判断
// 看一下三个if判断,分情况讨论:
// 一:如果线程池处于SHUTDOWN状态,1、firstTask不为空,那么返回false;2、firstTask为null且队列为空没必要创建 // 线程,返回false
// 但是调用execute方法进来的时候已经判断了firstTask不为空。那么这里只说明了一个条件
// 线程池处于SHUTDOWN,下面就会拒绝任务
// 二:如果线程池处于hutdown状态及其之后的几个状态,那么直接不接受任务
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&firstTask == null && !workQueue.isEmpty()))
return false;
// 再见死循环,Doug Lea就好这一口!
for (;;) {
int wc = workerCountOf(c);
// 几乎可以忽略第一个判断,提示:五亿多个线程。直接看第二个判断
// 根据core,来决定判断使用的是corePoolSize还是maximumPoolSize,核心线程和非核心线程
// 如果创建核心线程时,活跃线程数大于核心线程数量,直接返回FALSE;
// 如果创建非核心线程时,活跃线程数大于最大线程数量,直接返回FALSE;
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 说明满足情况,那么尝试线程池中添加一个线程,数量上做了一个标记
if (compareAndIncrementWorkerCount(c))
// 跳出循环,继续向下执行!所以说前半部分是判断
break retry;
// 上面添加线程个数没有成功,再次判断
c = ctl.get(); // Re-read ctl
// 如果当前状态和之前状态不一样,说明线程池状态发生了改变,继续循环
// 到底是RUNNING还是SHUTDOWN及其之后的判断之后决定是否需要加入到线程池中来
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// 线程没有开始工作
boolean workerStarted = false;
// 线程还没有添加到线程池中
boolean workerAdded = false;
Worker w = null;
try {
// 创建新的worker,本质上是线程类对象。
// 每个Worker对象都会创建一个新的线程。这里可以跳到下面的worker类查看
w = new Worker(firstTask);
// 获取工厂生产出来的线程对象
final Thread t = w.thread;
if (t != null) {
// 因为下面的workers(工作线程)、largestPoolSize和workerAdded都需要同步来进行执行,所以加锁!
// 这里考虑的是为了共享变量的原子性操作!创建的时候不允许中断
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
// 如果是running状态 or 处于SHUTDOWN状态且第一个任务为空。那么检查线程如果是存活的,直接抛出异常
// 新创建的线程没有启动,不会是存活的。那么现在就剩下两种情况:1、running和shutdown
// shutdown状态下,不接受新的任务,但可以处理队列中的任务
// 注意:前面有直接在线程池中添加线程的情况!
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 如果是线程工厂创建出来的线程是在运行的,显然线程还没有在下面启动
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// 向线程集合中添加当前线程
// 需要加锁
workers.add(w);
// 记录当前线程池中出现过的最大线程数量
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
// 记录worker已经添加成功
workerAdded = true;
}
} finally {
// 释放锁结点,表示已经worker节点各种同步操作完成
mainLock.unlock();
}
// 添加线程成功,那么需要开始执行
if (workerAdded) {
// 执行到了这一步,那么肯定会执行worker中的run方法!
// 注意这里的t是线程工厂创建出来的
t.start();
// 启动之后,需要来进行标记worker已经启动了
workerStarted = true;
}
}
} finally {
if (! workerStarted)
// 如果线程没有执行成功,那么添加失败
addWorkerFailed(w);
}
return workerStarted;
}
7.3、Worker类
两个重要的点:
1、实现Runnable接口,说明Worker类是一个线程类,那么重点关注run方法即可;
2、继承AQS(抽象队列同步器)并重写了其中的方法,为当前的线程池量身打造的;
首先看一下Worker的构造函数
这里是为了保持一个线程在同一个时间内只会执行一个任务,而不能够重入,这就是重写lock方法的目的
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
// 处理任务的线程,由工厂生产出来的
final Thread thread;
// 当前worker运行的第一个任务
Runnable firstTask;
// 当前worker完成的任务总数
volatile long completedTasks;
// Worker类重写了lock加锁的条件
Worker(Runnable firstTask) {
// 看注释:在runWorker方法执行之前,禁止中断!也就是说线程还没有创建好,不能去中断
// 这里的state就是AQS中的state状态值,用来表示锁的!因为Worker实现AQS之后重写AQS中的一些方法
// 抛出问题:为什么要重写AQS?
setState(-1); // inhibit interrupts until runWorker
// 传进来的任务作为当前线程执行的任务
this.firstTask = firstTask;
// 创建线程的时候,回想一下我们自己new Thread()的之后,怎么重写重写run方法的
// 这里传递进来的当前worker类对象,是一个Runnable接口的子类,那么当前线程需要执行的就是里面的run方法
this.thread = getThreadFactory().newThread(this);
}
}
所以Worker本身就是一个线程,那么作为一个线程,更多的应该关注的是其中的run方法中执行的是什么任务!!
进而可以推测出来:线程池中的每个线程都是由Worker对象组成的,所以ThreadPool其实维护的就是一组Worker对象
这里有个问题需要来进行说明一下:
7.3、Worker类为什么要来继承AQS
-
lock方法一旦获取了独占锁,表示当前线程正在执行任务中;
-
如果正在执行任务,则不应该中断线程;
-
如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;
-
线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;
-
之所以设置为不可重入,是因为不希望任务在调用像setCorePoolSize这样的线程池控制方法时重新获取锁。如果使ReentrantLock,它是可重入的,这样如果在任务中调用了如setCorePoolSize这类线程池控制的方法,会中断正在运行的线程。
所以,Worker继承自AQS,用于判断线程是否空闲以及是否可以被中断。
7.4、state变量
继承了AQS之后,可以看一下重写了什么?可以看到tryAcquire方法
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
可以看到这里改造了,改造成了独占模式,而不再是类似lock锁的可重入模式了。不过也很好理解,因为一个线程在执行一个任务的时候,不可能让另外一个线程也来进行执行。
但是可以看到在构造方法汇中将state变量设置成了-1,为了防止线程中断,然后在runWorker方法中会看到有unlock()方法来对state进行+1,最终将state变成0,这时候才代表接下来就可以来对线程进行中断了。
那么刚刚已经说过了,使用的是默认的线程工厂创建的,也就是DefaultThreadFactory创建的,看一下方法
终于可以看到创建线程的地方了:
public Thread newThread(Runnable r) {
// 创建一个新的线程并对名称进行命名方法。
Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);
// 将线程方式设置非守护式线程
// JAVA规定了当所有非守护线程退出时,JVM才会退出
// 但是这里设置的是非守护式线程,所以在主线程中执行的时候线程会一直在阻塞等待执行
if (t.isDaemon())
t.setDaemon(false);
// 设置成一样的优先级,也就是说线程池的线程都是一样的优先级级别
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
那么需要更加注意的是,当前的线程中,保存的任务是worker类,那么当线程执行的时候肯定会去执行worker类的run方法,这个是毋庸置疑的。
7.5、run方法
public void run() {
// 传递的是当前的对象,也就是worker本身
runWorker(this);
}
那么继续跟进:
final void runWorker(Worker w) {
// 获取得到当前线程!工厂创建出来的,而不是worker对象
Thread wt = Thread.currentThread();
// 获取得到worker中携带的任务
Runnable task = w.firstTask;
// 将任务置为空,任务对象执行完成了
w.firstTask = null;
// 在创建worker的时候,将state设置成了-1,这里将把state改成0,表示线程可以中断
w.unlock(); // allow interrupts
// 是否是因为异常引起的中断标记!
boolean completedAbruptly = true;
try {
// 从这个while循环就可以看到为什么说线程池一直可以等待任务进来进行调用
// 从这里可以看到同样有两种情况:
// 1、如果线程不为空,那么执行任务
// 2、线程池中的线程从队列中来获取得到任务执行 [因为存在着阻塞队列,循环+阻塞=线程复用]
// 有一种情况:线程池中的线程在从队列中获取得到任务的时候发现获取不到任务了跳出循环
while (task != null || (task = getTask()) != null) {
// 执行期间上锁!不允许重入,一个线程在同一个时刻只能够执行一个任务
w.lock();
// 如果线程池处于stop状态,那么要给当前线程池中的线程全部打上中断标记
// new:如果线程池正在停止,那么要保证当前线程是中断状态;如果不是,那么保证线程不是中断状态;
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted())
wt.interrupt();
try {
// 执行前需要做的事儿!开发者自定义
beforeExecute(wt, task);
// 执行线程任务发生异常错误
Throwable thrown = null;
try {
// 注意两个点!!!
// 1、worker类运行的是线程工程创建出来的run方法
// 2、异常处理机制
// 差一点点忽略了这个问题!!这就是为什么任务需要继承Thread类或者是实现Runnalbe方法
// 这里是直接调用任务对象的run方法来进行执行
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
// 执行后需要做的事儿!用户自定义
afterExecute(task, thrown);
}
} finally {
// 这里需要看一下!将任务执行完成将会置为null!这里是为了让线程执行完成之后,继续从队列中进行获取
task = null;
// 当前线程完成的任务数量
w.completedTasks++;
// 执行完成!解锁,表示任务结束
w.unlock();
}
}
// 出现了异常之后,这里不会执行
completedAbruptly = false;
} finally {
// 出现了异常之后,completedAbruptly为TRUE
processWorkerExit(w, completedAbruptly);
}
}
补充说明一下上面的stop状态:
根据线程池的stop状态:不接受新的任务,任务队列中不接受新的任务,并且会中断当前线程池中正在处理任务的线程。
当线程池状态处于RUNNING或者是SHUTDOWN状态的时候,调用shutdownNow方法的时候会进入到stop状态。
STOP状态要中断线程池中的所有线程,而这里使用Thread.interrupted()来判断是否中断是为了确保在RUNNING或者SHUTDOWN 状 态 时线程是非中断状态的,因为Thread.interrupted()方法会复位中断的状态。
所以这里还有两个没有看,一个是从对列中获取得到任务的,另外一个就是线程执行任务中出现了异常的处理机制
7.6、getTask
获取不到任务,就减少一个线程【所以对于核心线程或者是非核心线程都可以减少,可以设置】。
private Runnable getTask() {
// 上一次从队列中获取得到任务超时布尔值
boolean timedOut = false; // Did the last poll() time out?
// 二狗爱写死循环
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 如果线程池状态大于等于SHUTDOWN状态,如果是stop状态,或者是队列为空,那么不需要来执行任务,直接返回Null
// 1、处于SHUTDOWN状态,队列为空,那么没有线程池中的任务继续执行就行了。获取不到任务了
// 2、stop状态的,所有的任务都不会执行,并且会中断在执行的任务(在上一步中可以看到)
// 1、在线程池状态是SHUTDOWN之后,表示不接受新任务了
// 如果是STOP状态且队列为空,直接返回null,不接收新的任务执行
// 如果是SHUTDOWN状态直接返回null,接收新任务执行
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
// 尝试减少一个线程数。返回任务为空
decrementWorkerCount();
return null;
}
// 计算工作中的线程
int wc = workerCountOf(c);
// 核心线程超时默认为false。
// 那么当活跃线程数大于核心线程数,就可以认为是非核心线程数超时需要被清理掉
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 当前线程大于最大线程数(可能对当前线程池执行setMaximumPoolSize方法)
// timed && timedOut:当前操作需要进行超时操作并且上次已经超时了
// 并且当前线程池中的线程数大于1或者队列为空
// 这里是要表达的是线程长期处于饥饿状态,去掉线程
// 这里的目的是为了保证线程池中的有效线程节点个数。
// 第一轮循环:timedOut默认为FALSE,就代表了后面面这个判断不成立
// 第二轮循环时,为空的时候如果也为true,如果队列为空或者核心线程数大于1,那么直接移除,保留一个线程即可
if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
// 线程数-1失败,重试!
continue;
}
try {
// 判断是否超时的!如果是默认的话,那么是TRUE,那么使用poll方法获取的时候,如果获取不到,返回null;如果获取 // 到了,不为null
// 如果说没有对核心线程超时设置,当线程数大于核心线程数量时
// poll表示从移除队列首元素,队列为空返回null;如果超时,也返回为空(阻塞队列无任务)
// take表示的是拿不到就阻塞在这里
Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
if (r != null)
return r;
// 如果获取得到null,表示当前线程池中有线程处于闲置状态,那么考虑移除
timedOut = true;
// 如果发生了中断,那么这里可以看到进行重试
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
由上文中的分析可以知道,在执行execute方法时,如果当前线程池的线程数量超过了corePoolSize且小于maximumPoolSize,并且workQueue已满时,则可以增加工作线程,但这时如果超时没有获取到任务,也就是timedOut为true的情况,说明workQueue已经为空了,也就说明了当前线程池中不需要那么多线程来执行任务了,可以把多于corePoolSize数量的线程销毁掉,保持线程数量corePoolSize即可
线程池中的线程什么时候会销毁?当然是runWorker方法执行完之后,也就是Worker中的run方法执行完,由JVM自动回收。
getTask方法返回null时,在runWorker方法中会跳出while循环,然后会执行processWorkerExit方法
7.7、processWorkerExit
如果从队列中获取不到任务或者是因为线程执行代码是因为异常的时候:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 如果是因为中断引起的,那么减少一个
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 标记位已完成,虽然是已经执行了的!
// 这里是统计整个线程池中已经运行完成的任务数量
completedTaskCount += w.completedTasks;
// 从workers中移除掉一个线程,说明在线程池中少了一个工作的线程
workers.remove(w);
} finally {
mainLock.unlock();
}
// 根据线程池状态进行判断是否结束线程池
tryTerminate();
// 如果线程还在运行
int c = ctl.get();
// 如果线程池状态是running或者是shutdown状态
if (runStateLessThan(c, STOP)) {
// 因为异常走到这里来取反为FALSE,如果不是异常而且队列中无任务,那么走下面来
if (!completedAbruptly) {
// 如果允许核心线程超时,那么一个线程池中留下来一个线程执行;如果不允许,那么最小是核心线程数量
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
// 如果没有线程,但是队列不为空,那么进行赋值为1.至少留一个线程
if (min == 0 && ! workQueue.isEmpty())
min = 1;
// 如果工作线程大于等于1,最小值,那么什么都不做
if (workerCountOf(c) >= min)
return; // replacement not needed
}
// 但是如果是因为中断引起的,那么抛弃掉线程,因为上面已经将Worker给删除了,这里重新创建一个非核心线程
// 创建一个非核心线程,没有第一个任务,需要从队列中来进行获取,重跑
addWorker(null, false);
}
}
这里也就是在说明,如果线程池中线程执行异常,那么将线程丢弃掉,然后创建新的线程。但是也表明了将出问题的线程任务给丢弃了
因为任务保存在while循环之上,所以这里的任务是可以重新使用的。
对于每个线程来说,都是拥有第一个任务的,跑完第一个任务之后,那么接下来就需要从任务队列中来获取得到新的任务来进行执行。
因为使用的是while循环+阻塞队列这种操作方式的话,将会导致线程池中的线程不断的去从阻塞队列中来获取得到任务来进行操作。
如果获取不到,那么将会导致线程阻塞;
所谓的线程池的等待时间就是在这里的操作,如果在指定的时间中没有获取得到对应的任务,可以考虑设置对应的非核心线程的超时时间,将非核心线程去掉,那么当前的线程的销毁就会交给JVM来进行垃圾回收。
8、线程池参数设置
CPU密集型:CPU核数+1;【计算型类的】
IO密集型:2*CPU核数+1;【设计到文件、网络等等操作】
rocketmq、eureka等都是以2*CPU核数设置的。
最好是自定义来进行设置:100+1000
9、线程池中的方法
workerCountOf
获取工作线程数量【非线程池方法】
getCorePoolSize
获取核心线程数,这不是显示当前的线程数,是总共可以创建多少个核心线程数
getLargestPoolSize
之前线程池中,存在过的最大线程的数目
getMaximumPoolSize
获取当前线程池允许创建的最大线程数
getPoolSize
获取当前存活在线程池中的线程的个数
getQueue
获取当前队列中存在的任务
getCompletedTaskCount
获取曾经执行完成过的任务大致总数,为啥是大致呢?因为你在调用的时候,要是刚好有个线程完成任务,那数量就变化了
getTaskCount
获取当前线程池曾经计划执行的个数,启动的时候就算数的。大致任务数量
getRejectedExecutionHandler
获取当前线程池的拒绝策略
getActiveCount
返回正在主动执行任务的线程的大致数量。因为可能在一瞬间执行任务的线程数量发生了变化。
9、示例代码
1、拒绝策略的设置
线程池的定义
一个核心线程,两个最大线程,阻塞队列最大容量是1,非核心线程数在1秒钟处理不到任务之后销毁。
默认采用的线程决绝策略是AbortPolicy,也就是直接拒绝任务。所以说要是想不来拒绝任务,建议阻塞队列采用链表阻塞队列。
public class MyThreadPoolExecutor {
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(1,2,1, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(1));
public static <T> T executeTask(Runnable runnable){
return (T) THREAD_POOL_EXECUTOR.submit(runnable);
}
}
对应的示例代码:
public class ThreadPoolTestTwo {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
MyTask myTask = new MyTask();
MyThreadPoolExecutor.executeTask(myTask);
}
}
static class MyTask implements Runnable{
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
控制台打印输出:
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@14ae5a5 rejected from java.util.concurrent.ThreadPoolExecutor@7f31245a[Running, pool size = 2, active threads = 2, queued tasks = 1, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
at com.guang.threadpool.test.MyThreadPoolExecutor.executeTask(MyThreadPoolExecutor.java:20)
at com.guang.threadpool.test.ThreadPoolTestTwo.main(ThreadPoolTestTwo.java:8)
pool-1-thread-1
pool-1-thread-2
pool-1-thread-1
因为一共就有两个最大线程,核心线程接收第一个任务来执行之后,第二个任务提交到队列中一个排队,然后第三个任务被非核心线程处理,后来的任务因为阻塞队列满了之后,直接给拒绝掉不来进行执行。
2、线程执行任务发生异常
如果说线程池中的线程执行代码期间发生了异常,那么会对线程池造成影响吗?答案是会,什么影响呢?看一下代码:
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 抛出异常
task.run();
} catch (RuntimeException x) {
// 向外抛
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 最终到这里来,那么这个线程执行任务出现异常,completedAbruptly=TRUE
processWorkerExit(w, completedAbruptly);
}
看看processWorkerExit中的处理方式:
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
首先线程池中的运行的工作线程减少一个,然后添加一个非核心线程从阻塞队列中获取执行:
addWorker(null, false);
写段代码来看一下:
public class ThreadPoolTestException {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
10,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10));
System.out.println(String.format("活跃的线程数有:%s 个",+threadPoolExecutor.getActiveCount()));
threadPoolExecutor.execute(new MyTask());
System.out.println(String.format("活跃的线程数有:%s 个",+threadPoolExecutor.getActiveCount()));
threadPoolExecutor.shutdownNow();
}
static class MyTask implements Runnable{
@Override
public void run() {
int i = 1 / 0;
System.out.println(Thread.currentThread().getName());
}
}
}
控制台输出:
活跃的线程数有:0 个
活跃的线程数有:1 个
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
at com.guang.threadpool.test.ThreadPoolTestException$MyTask.run(ThreadPoolTestException.java:22)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
为什么还有一个线程执行?因为最终创建了一个非核心线程在运行。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器