ThreadPoolExecutor线程池原理+源码,了解—下?

这两种方式开启的线程都不便于统一的调度和管理
HotSpot虚拟机采用1:1的模型来实现Java线程的,也就是说一个Java线程直接通过一个操作系统线程来实现,如果可以无限制的开启线程,很容易导致操作系统资源耗尽。

线程池

继承Thread和实现Runnable的诸多缺点,所以生产环境必须使用线程池来实现多线程。

线程池(thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 ——维基百科

简单来说,“池”在计算机领域是指集合,线程池就是指线程集合。线程池可以对一系列线程的生命周期进行统一的调度和管理,包括线程的创建、消亡、生存时间、数量控制等。
Java中的线程池从JDK1.5开始,有一个标准的实现java.util.concurrent.ThreadPoolExecutor,对于这个类,首先看下它的体系结构图
ThreadPoolExecutor类结构体系

  • Executor:只定义了一个方法execute,用于执行提交的任务
  • ExecutorService:定义了一些线程池管理、任务提交、线程池检测的方法
  • AbstractExecutorService:提供了ExecutorService接口执行方法的默认实现,用于统一处理Callable任务和Runnable任务

内部结构

这里主要关注类的定义和一些重要的常量、成员变量

public class ThreadPoolExecutor extends AbstractExecutorService {

// 高3位表示线程池状态,低29位表示worker数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 29 = 32 - 3
private static final int COUNT_BITS = Integer.SIZE - 3;
// 线程池允许的最大线程数。为 2^29 - 1
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
// 线程池有5种状态,按大小排序如下:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

// Packing and unpacking ctl
// 获取线程池状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 获取线程池worker数量
private static int workerCountOf(int c) { return c & CAPACITY; }
// 根据线程池状态和worker数量生成ctl值
private static int ctlOf(int rs, int wc) { return rs | wc; }

// 缓冲队列(阻塞队列)
private final BlockingQueue workQueue;

// 互斥锁
private final ReentrantLock mainLock = new ReentrantLock();

// 包含线程池工作的所以线程,仅在持有mainLock的时候能访问
private final HashSet workers = new HashSet();

private final Condition termination = mainLock.newCondition();

// 跟踪线程池最大的大小(实际的最大值),仅在持有mainLock的时候能访问
private int largestPoolSize;

// 记录已经完成的任务数,仅在工作线程终止时更新,仅在持有mainLock的时候能访问
private long completedTaskCount;

// 线程工厂
private volatile ThreadFactory threadFactory;

// 线程池饱和或者关闭时的执行器
private volatile RejectedExecutionHandler handler;

// 空闲线程等待工作的超时时间
private volatile long keepAliveTime;

// 如果为false(默认值),核心线程永远不回收
// 如果为true,核心线程也通过keepAliveTime参数超时回收
private volatile boolean allowCoreThreadTimeOut;

// 核心线程数
private volatile int corePoolSize;

// 最大线程数(程序设置的最大线程数,区别于largestPoolSize)
private volatile int maximumPoolSize;

// 默认的拒绝策略处理器,抛出RejectedExecutionException异常
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
}

涉及到的成员变量、常量比较多,也不太容易理解,不过看完整篇后再来回顾这里,就很容易理解了。

生命周期

ThreadPoolExecutor类提供了线程池的五个状态描述

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

这几种状态之间的转换过程如下
线程池生命周期

  • RUNNING:运行状态,可以执行任务,也可以接受阻塞队列里的任务调度
  • SHUTDOWN:调用了shutdown()方法,该状态可以继续执行阻塞队列中的任务,但是不会再接受新任务
  • STOP:调用了shutdownNow()方法,该状态会尝试中断正在执行的所有任务不能继续执行阻塞队列中的任务,也不会再接受新任务
  • TIDYING:所有任务都执行完毕,至于阻塞队列中的任务是否执行完成,取决于调用了shutdown()还是shutdownNow()方法
  • TERMINATEDterminated()方法执行完成后进入该状态,terminated()方法默认没有任何操作

构造方法

ThreadPoolExecutor提供了四个构造方法,忽略它提供的语法糖,我们直接看最吊的那个构造方法:

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
// corePoolSize、maximumPoolSize、keepAliveTime都不能小于0
// 且maximumPoolSize必须大于等于corePoolSize
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
// workQueue、threadFactory、handler均不能为null
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

这个构造方法有七个参数,如果能明白各个参数的作用,那么线程池的工作原理也就基本清晰了。

  • int corePoolSize:核心线程数,当有新的任务提交到线程池时,会进行如下判断:

  • 线程池中线程数量小于corePoolSize时,会创建新线程处理任务,即使还有其他空闲的核心线程

  • 线程池中线程数量等于corePoolSize时,任务会加入到workQueue缓存队列,直到缓存队列满了,才会新建非核心线程去处理任务

  • 线程池中的线程数量等于maximumPoolSize缓存队列已满时,会根据RejectedExecutionHandler参数指定的拒绝策略来处理提交的任务

  • 如果corePoolSizemaximumPoolSize相等,则创建的线程池大小是固定的,缓存队列满了就执行决绝策略

  • int maximumPoolSize:最大线程数

  • long keepAliveTime非核心线程的最长空闲时间,超过了会被回收(allowCoreThreadTimeOut参数设置成true,也会回收核心线程)

  • TimeUnit unitkeepAliveTime参数的单位

  • BlockingQueue<Runnable> workQueue:阻塞队列,用于缓存,保存正在等待执行的任务。一般有以下几种配置

  • 直接切换:常用的队列是SynchronousQueue

  • 无界队列:常用的队列是LinkedBlockingQueue,队列基于链表实现,最大长度是Integer.MAX_VALUE,虽然是有界的,但是值太大,所以认为是无界队列。使用无界队列可能会导致最大线程数maximumPoolSize失效,这点结合下文的线程池执行过程会很容易理解

  • 有界队列:常用的队列是ArrayBlockingQueue(,基于数组实现,能把最大线程数控制为maximumPoolSize。也能避免阻塞队列中堆积的任务过多。

  • ThreadFactory threadFactory:线程Factory,用来创建线程。使用默认的ThreadFactory创建的线程是具有相同优先级的非守护线程。一般需要自定义ThreadFactory,因为要给每个线程设置有意义的名称

  • RejectedExecutionHandler handler: 当线程数达到了最大线程数,且没有线程空闲,且缓冲队列也满了(也就是线程池饱和了),指定拒绝策略,ThreadPoolExecutor自身提供了四种拒绝策略:

  • AbortPolicy:直接抛出java.util.concurrent.RejectedExecutionException异常

  • CallerRunsPolicy:利用调用者所在的线程执行任务,哪个线程提交这个任务,就由哪个线程执行

  • DiscardOldestPolicy:丢弃缓存队列中头部的任务,重试提交的任务

  • DiscardPolicy:直接丢弃

显然默认的四种拒绝策略都不能很好的使用在生产环境,所以一般也需要自定义拒绝策略来处理饱和的任务。将暂时无法处理的任务存入中间件、数据库以及日志记录。

线程池中线程的数量并不是越多越好,因为服务器的性能总是有限的。线程数过多会增加线程切换的开销,并且空闲线程的频繁回收也需要消耗资源。线程池的七个参数相辅相成,相互影响,设置的时候需要根据实际情况酌情考虑。
看文字描述多少有些不清晰,如果能有张图的话就再好不过了。你就说巧不巧吧,刚好我画了一张图。
线程池执行原理

对照这张图和上面的描述,相信大家对ThreadPoolExecutor的七个参数有个深刻的认识。也很容易理解为什么使用无界队列LinkedBlockingQueue会使maximumPoolSize失效了,因为缓存队列可能永远不会满

核心方法

毫无疑问,线程池最核心的方法除了构造方法,就是执行task的方法了。在看ThreadPoolExecutor的核心方法之前,先看一个非常非常重要的内部类Worker,它是线程池中运行任务的最小单元。

// 继承了AbstractQueuedSynchronizer,是一把锁
// 实现了Runnable接口,是一个线程执行的task
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{

private static final long serialVersionUID = 6138294804551838833L;

/** 运行任务的线程 /
final Thread thread;
/
* 要运行的初始任务,可能为null /
Runnable firstTask;
/
* 每个线程的任务计数器 */
volatile long completedTasks;

Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
// 把自己作为一个任务传递给ThreadFactory创建的线程
this.thread = getThreadFactory().newThread(this);
}

/** runWorker是一个非常重要的方法,后文详细介绍 */
public void run() {
runWorker(this);
}

// 值为0代表解锁状态
// 值为1表示锁定状态
protected boolean isHeldExclusively() {
return getState() != 0;
}

// CAS的方式尝试加锁
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}

// 尝试释放锁
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}

public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }

void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}

Worker类实现了Runnable接口,所以本身就是一个可执行的任务,并且在构造方法中将自己传递给ThreadFactory创建的线程去执行
Worker类继承了AbstractQueuedSynchronizer类,所以它本身也是一把锁,执行任务的时候锁住自己,任务执行完成后解锁。
了解了Worker类,再来看核心方法。

execute

execute方法用于在将来的某个时间执行指定的任务execute方法源码比较复杂,应该先理清楚整体逻辑,在逐步深入细节。

public void execute(Runnable command) {
if (command == null)
// 提交空任务,直接抛异常
throw new NullPointerException();

int c = ctl.get();
if (workerCountOf© < corePoolSize) {
// worker数量小于核心线程数,创建核心线程执行任务(第二个参数为true,表示创建核心线程)
// addWorker方法会检查线程池的状态
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning© && workQueue.offer(command)) {
// worker数量超过核心线程数,进入缓冲队列
// 再次获取ctl值,因为从上次获取到这里,有可能ctl的值已经被改变,double-check
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
// 线程池不是RUNNING状态,说明已经调用过shutdown方法,需要对新提交的任务执行拒绝策略
reject(command);
else if (workerCountOf(recheck) == 0)
// 因为构造方法中corePoolSize可能为0或者核心线程也都被回收了,所以此处需要判断
addWorker(null, false);
}
else if (!addWorker(command, false))
// 线程池不是RUNNING状态,或者任务加入缓冲队列失败,创建非核心线程执行任务(第二个参数为false)
// 任务执行失败,需要执行拒绝策略
reject(command);
}

整体逻辑就是前文所示的流程图。相信有了流程图的对比,execute方法的理解就容易多了。

addWorker

addWorker方法用于往线程池添加新的worker。其实现如下:

private boolean addWorker(Runnable firstTask, boolean core) {
retry: // 这种写法叫做label语法,一般用于多重性循环中跳转到指定位置
for (;😉 {
// 外层自旋
int c = ctl.get();
int rs = runStateOf©;

// Check if queue empty only if necessary.
// 线程池状态 >= SHUTDOWN
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;😉 {
// 内层自旋
int wc = workerCountOf©;
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
// 工作中的线程数大于线程池的容量,或者已经大于等于核心线程数,或者大于等于最大线程数
// core为true,表示要创建核心线程,false表示要创建非核心线程
// 为什么大于等核心线程数的时候要返回false,因为要添加到缓冲队列,或者创建非核心线程来执行,不能创建核心线程了
return false;
if (compareAndIncrementWorkerCount©)
// 以CAS的方式尝试把线程数加1
// 注意这里只是把线程池中的线程数加1,并没有在线程池中真正的创建线程
// 成功后跳出内层自旋
break retry;
// CAS失败,再次获取ctl,检查线程池状态
c = ctl.get(); // Re-read ctl
if (runStateOf© != rs)
// 线程池状态被改变了,从外层自旋开始再次执行之前的逻辑
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// 可以看到两层自旋 + CAS,仅仅是为了把线程池中的线程数加1,还没有新建线程

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 把task包装成Worker
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
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());

if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
// 检查线程状态
throw new IllegalThreadStateException();
// 添加到worders
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
// 维护largestPoolSize变量
largestPoolSize = s;
workerAdded = true;
}
} finally {
// 解锁
mainLock.unlock();
}
if (workerAdded) {
// 添加成功
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
// 执行worker的线程启动失败
addWorkerFailed(w);
}
return workerStarted;
}

可以看到addWorker方法前一部分,用了外层自旋判断线程池的状态,内层自旋 + CAS给线程池中的线程数加1。后半部分用了ReentrantLock保证创建Worker对象,以及启动线程的线程安全。一个方法中三次获取了线程池的状态(不包含该方法调用的其他方法),因为每两次之间,线程池的状态都有可能被改变。

runWorker

前文在介绍Worker内部类时说过,Worker会把自己传递给ThreadFactory创建的线程执行,最终执行Workerrun方法,而Worker类的run方法只有一行代码:

总结

面试难免让人焦虑不安。经历过的人都懂的。但是如果你提前预测面试官要问你的问题并想出得体的回答方式,就会容易很多。

此外,都说“面试造火箭,工作拧螺丝”,那对于准备面试的朋友,你只需懂一个字:刷!

给我刷刷刷刷,使劲儿刷刷刷刷刷!今天既是来谈面试的,那就必须得来整点面试真题,这不花了我整28天,做了份“Java一线大厂高岗面试题解析合集:JAVA基础-中级-高级面试+SSM框架+分布式+性能调优+微服务+并发编程+网络+设计模式+数据结构与算法等”

image

且除了单纯的刷题,也得需准备一本【JAVA进阶核心知识手册】:JVM、JAVA集合、JAVA多线程并发、JAVA基础、Spring 原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB、Cassandra、设计模式、负载均衡、数据库、一致性算法、JAVA算法、数据结构、加密算法、分布式缓存、Hadoop、Spark、Storm、YARN、机器学习、云计算,用来查漏补缺最好不过。

image

得来整点面试真题,这不花了我整28天,做了份“Java一线大厂高岗面试题解析合集:JAVA基础-中级-高级面试+SSM框架+分布式+性能调优+微服务+并发编程+网络+设计模式+数据结构与算法等”

[外链图片转存中…(img-qgWUfxBp-1714478880574)]

且除了单纯的刷题,也得需准备一本【JAVA进阶核心知识手册】:JVM、JAVA集合、JAVA多线程并发、JAVA基础、Spring 原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB、Cassandra、设计模式、负载均衡、数据库、一致性算法、JAVA算法、数据结构、加密算法、分布式缓存、Hadoop、Spark、Storm、YARN、机器学习、云计算,用来查漏补缺最好不过。

[外链图片转存中…(img-KJWzsS8j-1714478880574)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值