一,前言
大家好,我是小墨,这期介绍线程池。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
在创建线程池Executors类中有四种建立线程池的方法,分别为:
- newSingleThreadPool:创建一个单线程的线程池,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变
- newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求
但我们注意底层其实创建线程池是使用ThreadPoolExecutor类来进行线程池的操作,我们先简单介绍ThreadPoolExecutor各个参数,然后来说下其底层代码如何实现
二,ThreadPoolExecutor使用介绍
我们先来一段简单代码看下如何使用,我们看到有几个具体参数设置,比较重要。
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor excutors = new ThreadPoolExecutor(
GatewayConfig.HTTP_SERVER.PRE_DISPATCHER_THREAD_SIZE / 3,
GatewayConfig.HTTP_SERVER.PRE_DISPATCHER_THREAD_SIZE,
3, TimeUnit.MINUTES,
new LinkedBlockingDeque<Runnable>(GatewayConfig.HTTP_SERVER.QUEUE_LENGTH),
new ThreadFactory() {
private volatile int index = 1;
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
index++;
thread.setName("dispatch_worker_" + index);
return thread;
}
}
);
excutors.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (r instanceof Rejectable) {
Rejectable reject = (Rejectable) r;
reject.reject();
}
}
});
for (int i = 0; i < 5; i++) {
executor.submit(new DivTask(100, i));
}
}
static class DivTask implements Runnable {
int a, b;
public DivTask(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
double re = a / b;
System.out.println(re);
}
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数分别为:
corePoolSize 核心线程池数
- 核心线程会一直存活,即使没有任务需要执行
- 当设置此值时,初始化线程池时会创建一定数量的线程放入线程池中,后面线程需要时可以直接调用,而不需要再创建,设置为0则不会预先创建线程。
maximumPoolSize 线程池最大容量
- 当线程数>corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务,直到线程数量达到maxPoolSize
- 当线程数已经=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
keepAliveTime 线程池空闲时,线程存活的时间
- 当线程空闲时间达到keepAliveTime时,线程会被销毁,直到线程数量=corePoolSize
TimeUnit 时间单位
ThreadFactory 线程工厂:可以设置线程创建名字等。
BlockingQueue任务队列:提交的任务会放置入任务队列中,进行排队,任务队列包括无限队列和有限队列。
RejectedExecutionHandler 线程拒绝策略,
两种情况会拒绝处理任务:
1、当线程数已经达到maxPoolSize,且任务队列已满时,会拒绝新任务
2、当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务(并不是立马停止,而是执行完再停止)。
若拒绝后,此时,线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置,默认值是AbortPolicy,会抛出异常
hreadPoolExecutor类有几个内部实现类来处理这类情况:
1: AbortPolicy 丢弃任务,抛运行时异常
2:CallerRunsPolicy 执行任务(这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功) 如果执行器已关闭,则丢弃.
3:DiscardPolicy 对拒绝任务直接无声抛弃,没有异常信息
4:DiscardOldestPolicy 对拒绝任务不抛弃,而是抛弃队列里面等待最久的(队列头部的任务将被删除)一个线程,然后把拒绝任务加到队列(Queue是先进先出的任务调度算法,具体策略会咋下面有分析)(如果再次失败,则重复此过程)
5:实现RejectedExecutionHandler接口,可自定义处理器(可以自己实现然后set进去)
整个流程可以参考下图,概括如下
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满
若线程数小于最大线程数,创建线程
若线程数等于最大线程数,抛出异常,拒绝任务
我们常用的向线程池提交线程的方式有execute,submit,我们看下区别:
- execute提交的方式只能提交一个Runnable的对象,且该方法的返回值是void,无返回值
- submit() 的返回值 Future 调用get方法时,可以捕获处理异常。
三,源码解析
我们看下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;
}
我们主要关注下execute方法
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;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
代码主要分几步:
- 如果正在工作的线程小于corePoolSize,则会创建线程,然后返回。
- 当核心数已经达到corePoolSize时,则会判断是否能够放入阻塞队列,放入成功后,那么这时候可能还没到线程池的maxnum,所以尝试增加一个Worker。
- 如果放入不成功,说明达到最大线程池大小,拒绝。
我们看到这里的重点是addWorker方法,我们重点讲下
1)addWorker方法
Worker的增加和Task的获取以及终止都是在此方法中实现的
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
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 {
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();
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;
}
addWorker方法看起来很复杂,我们不每行代码一一介绍,我们抽丝剥茧可以得到主要代码如下,逻辑为:
- 循环中判断状态是否可以加入线程,略过不计
- 使用当前线程包装成Worker对象,因为使用hashSet<Worker> workers作为集合存放,所以使用可重入锁锁住,只允许一个线程去往workers加。
- 注意我们要注意到在这里能加入的线程为核心线程。
- 如果往workers加成功,t.start()启动,由操作系统去调用。
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
//.....
}
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 {
//.....
workers.add(w);
//....
}
finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
我们重点关注Worker对象,Worker对象实现了Runnable,继承了AbstractQueuedSynchronizer AQS
重写run方法,
public void run() {
runWorker(this);
}
runWorker方法为重点方法值得我们去分析。
2)runWorker方法-----重点关注
我们分析这个代码逻辑如下:
- 在循环中判断当前线程和阻塞队列中的线程(getTask方法)是否为空,
- 使用Worker类的lock方法自我加锁,不影响其他线程,也不允许其他线程影响,执行线程的run方法
- 使用模板模式,提供了beforeExecute,afterExecute给用户进行方法增强
- 这个线程一直保持,因为阻塞队列的take方法即使在阻塞队列为空也会阻塞,这部分不清楚的请去了解下。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
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 {
processWorkerExit(w, completedAbruptly);
}
}
我们注意到getTask()方法,我们也一起放上来,我们关注主要逻辑如下:
- wc参数标识当前Worker超时是否要退出,我们直到线程池可以设置超时时间,超时则取出最后放进去的线程。判断wc > corePoolSize。如果大于,需要减小空闲的Worker数,那么timed为true,阻塞队列要
根据超时时间是否到达来移除头结点,但是wc <= corePoolSize时,timed为false,取下最早放入的线程让线程脱离阻塞状态进入就绪状态。workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
2,关闭线程池
对于关闭线程池主要有两个方法shutdown()和shutdownNow():
我们比较下这两个方法区别:
shutdown:
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
判断是否能够操作,设置线程池状态为shutdown,中断所有空闲线程,不允许新的线程进入线程池,我们来看下什么是中断空闲线程。
中断空闲线程需要先判断是否中断,尝试去获取锁,那么正在运行的线程则获取不到,允许继续运行,而阻塞队列未运行的线程则可以顺利获取到锁,然后启动中断。
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
shutdownNow():
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
shutdownNow是调用interruptWorkers,拒绝所有新Task的加入,同时中断所有线程,WorkerQueue中没有执行的线程全部抛弃。所以此时Pool是空的,WorkerQueue也是空的。
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
四,总结
线程池核心是使用ThreadPoolExecutor类,利用阻塞队列来存储线程池,核心线程直接启动,当发现运行线程数小于核心线程数,从阻塞队列去线程运行。
题外话:
设计线程池数量
对于设置线程池大小其实是个需要综合业务来设计的情况。我们简单的考虑就是:
- 就是如果你是CPU密集型运算,那么线程数量和CPU核心数相同就好,避免了大量无用的切换线程上下文
- 如果你是IO密集型的话,需要大量等待,那么线程数可以设置的多一些,比如CPU核心乘以2.
参考文章
https://www.jianshu.com/p/ade771d2c9c0
https://juejin.im/post/6844903600393715719
【小家java】用 ThreadPoolExecutor/ThreadPoolTaskExecutor 线程池技术提高系统吞吐量(附带线程池参数详解和使用注意事项)