深入理解并发编程之线程池ThreadPoolExecutor源码分析
文章目录
一、线程池简介
1.为什么要使用线程池
因为频繁的开启线程或者停止,线程需要从新被cpu从就绪到运行状态调度,效率非常低。所以使用线程可以实现复用,从而提高效率。
2.线程池的作用
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行
3.线程池的创建方式
线程池有四种创建方式,这些线程池底层都是基于ThreadPoolExecutor构造函数封装的。
Executors.newCachedThreadPool(); 可缓存线程池
Executors.newFixedThreadPool();可定长度
Executors.newScheduledThreadPool() ; 可定时
Executors.newSingleThreadExecutor(); 单例
下面为线程池的使用代码:
public class Test01 {
public static void main(String[] args) {
// 底层中只会创建两个线程复用
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
executorService.execute(
new Runnable() {
@Override
public void run() {
//打印正在执行的缓存线程信息
System.out.println(Thread.currentThread().getName() + "正在被执行");
}
}
);
}
}
}
看一下执行的结果,线程池里面创建了两个线程,其实一直是这两个线程在复用,并不是创建了10个线程,这样减少了创建线程和销毁线程的消耗:
4.阻塞队列
我们点击进入newFixedThreadPool()方法发现线程池的实现是基于阻塞队列的,下面来介绍几种常用的阻塞队列:
- ArrayBlockingQueue: 有界队列,基于数组结构,按照队列FIFO原则对元素排序;
- LinkedBlockingQueue: 无界队列,基于链表结构,按照队列FIFO原则对元素排序,Executors.newFixedThreadPool()使用了这个队列; 默认是Integer.MAX_VALUE,有界则是可以自己定义.
- SynchronousQueue: 同步队列,该队列不存储元素,每个插入操作必须等待另一个线程调用移除操作,否则插入操作会一直被阻塞,Executors.newCachedThreadPool()使用了这个队列;
- PriorityBlockingQueue: 优先级队列,具有优先级的无限阻塞队列。
二、线程池原理分析
1.ThreadPoolExecutor代码分析
先看一下ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler) {
if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?null : AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
下面分别说一下这些参数的具体含义:
- corePoolSize: 核心线程数量,也就是一直正在保持运行的线程。
- maximumPoolSize: 最大线程数,线程池允许创建的最大线程数。
- keepAliveTime: 超出corePoolSize后创建的线程的存活时间。
- unit: keepAliveTime的时间单位
- workQueue: 任务队列,用于保存待执行的任务。
- threadFactory: 线程池内部创建线程所用的工厂。
- handler: 任务无法执行时的处理器。
线程池有五种状态:
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: 线程池不可以接受新任务,但是可以对已添加的任务进行处理。
- STOP: 线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
- TIDYING: 当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行构造函数。
- TERMINATED: 线程池彻底终止的状态。
2.ThreadPoolExecutor的逻辑
线程池接收线程任务调度的逻辑是这样的:
- 提交任务的时候比较核心线程数,如果当前任务数量小于核心线程数的情况下,则直接复用线程执行。
- 如果任务量大于核心线程数,则缓存到队列中。
- 如果缓存队列满了,且任务数小于最大线程数的情况下,则创建线程执行;如果队列且最大线程数都满的情况下,则走拒绝策略
我们看一下源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//如果当前任务数程数小于核心线程数
if (workerCountOf(c) < corePoolSize) {
//增加线程
if (addWorker(command, true))
return;
c = ctl.get();
}
//线程池是运行状态,并且缓存队列还能继续存放
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//线程池不是运行状态,将当前任务再从缓存队列中移除,并且拒绝任务
if (! isRunning(recheck) && remove(command))
reject(command);
//如线程池已关闭,且工作线程为0,则创建一个空闲工作线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//这里传的是false,则addWorker判断的是最大线程数,如果超出最大线程数就走拒绝策略
else if (!addWorker(command, false))
reject(command);
}
//增加核心线程操作
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get(); //读取当前任务数量
int rs = runStateOf(c);
//如果线程池不是停止状态 && !(线程池停止状态&&传入的线程为空&&队列不为空)直接返回false
if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
return false;
for (;;) {
//获取任务数量
int wc = workerCountOf(c);
//如果任务数量大于最大容量||或者任务数量大于核心线程数量 直接返回false
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//CAS任务数量+1
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); //重新读取当前任务数量
if (runStateOf(c) != rs) //工作状态判断
continue retry;
}
}
//上面的都走完了,说明此时任务数量是小于核心线程数量的
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//以当前任务创建个线程
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); //这一步加锁了,防止并发
try {
int rs = runStateOf(ctl.get()); //获取当前工作状态
//如果工作状态为运行中 || 工作关闭传入的任务为空
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive())
throw new IllegalThreadStateException();
//核心线程容器增加当前任务
workers.add(w);
int s = workers.size();
//跟踪最大核心线程池的数量,这就是最后一步队列满了而最大线程数量没满
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
//如果核心线程增加成功,运行核心线程
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
再看一下线程池运行线程的方法,可以看出线程池的核心线程一直死循环运行,直到缓存线程为0或者线程池停止:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock();
boolean completedAbruptly = true;
try {
// getTask不为空,这是在任务队列里面获取一个任务,只要任务队列不为空则一直循环
while (task != null || (task = getTask()) != null) {
w.lock();
//状态判断,如果线程池停止则通过中断退出循环停止操作
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 {
processWorkerExit(w, completedAbruptly);
}
}
看完源码完整的逻辑图是这样的:
三、线程池常见问题
1.线程池队列拒绝策略
如果队列满了,且任务总数>最大线程数则当前线程走拒绝策略,拒绝策略是可以自定义的。
rejectedExecutionHandler:任务拒绝处理器
两种情况会拒绝处理任务:
- 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
- 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。
线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置,默认是AbortPolicy,会抛出异常。
ThreadPoolExecutor类有几个内部实现类来处理拒绝任务:
- AbortPolicy 丢弃任务,抛运行时异常
- CallerRunsPolicy 执行任务
- DiscardPolicy 忽视,什么都不会发生
- DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
- 实现RejectedExecutionHandler接口,可自定义处理器
2.线程池参数配置多少最合理
CPU密集型服务器: CPU密集,线程不会阻塞,一直在运行,线程代码非常快结束,则最大线程数配置与CPU核数相当就可以了,线程数正好为CPU内核数量是为了减少切换线程时的资源浪费。
IO密集型服务器: IO密集,比如读取IO、导致当前线程有可能阻塞,或者线程执行代码非常耗时,就可以多设置些线程,让等待的这段时间线程可以去做其他事,提高并发处理效率,则最大线程数配置为Cpu核数*2即可。
3.使用Executors的弊端
下面阿里的 Java开发手册中的一个强制:
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
因为使用的是无解队列,无界队列默认最大长度为Integer.MAX_VALUE,这是下面阿里规范提到的问题,因为无界情况下缓存的线程是无上限的,会导致内存溢出。