想要在高并发的环境下编写出高效稳定的应用程序,线程池的"内功"修炼是必不可少的一课。简单的使用线程池固然可以规避资源浪费、解决线程生命周期开销问题。但要彻底发挥线程池的最大潜能,我们必须对它的核心原理有深刻的理解和把握,才能实现线程管理的精细化调度,资源利用的最大化。让我们一起揭开神秘的面纱,解开线程池的奥秘吧!
java.util.concurrent
包中的ThreadPoolExecutor
类,它是线程池的核心,我们先来认识一下 ThreadPoolExecutor
类。
一、ThreadPoolExecutor 类概览
ThreadPoolExecutor
是一个通用的线程池执行器,它提供了丰富的配置选项和灵活的线程管理策略。以下是其构造函数的签名:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明:
corePoolSize
:核心线程池大小。maximumPoolSize
:最大线程池大小。keepAliveTime
:非核心线程空闲存活时间。unit
:keepAliveTime
的时间单位。workQueue
:一个阻塞队列,用于存放等待执行的任务。threadFactory
:用于创建新线程的工厂。handler
:当任务队列满了且线程池达到最大容量时,使用的饱和策略。
ThreadPoolExecutor
的源码实现涉及到线程的创建、任务的调度、线程的生命周期管理以及线程池的关闭等多个方面。虽然线程池为我们屏蔽了诸多线程管理的细节,但要真正驾驭它,只有通过深入理解这些源码实现,才能更好地使用线程池,优化并发程序的性能。
因此我们最终还是需要深入研究一下ThreadPoolExecutor的源码实现。
下面就让我们一窥线程池的内在运作机制。
二、剖析线程池的源码实现
1、任务存储队列
在ThreadPoolExecutor中,任务的存储和排队是由一个BlockingQueue实例来管理的。BlockingQueue是Java并发包中的一个接口,它支持两个附加操作:当队列已满时,试图入队的线程会被阻塞,直到队列有空闲位置;当队列为空时,试图出队的线程也会被阻塞,直到有新元素入队。
BlockingQueue有多种不同的具体实现,比如无界队列LinkedBlockingQueue、有界队列ArrayBlockingQueue、按优先级排序的PriorityBlockingQueue等。我们可以根据实际场景的需求选择合适的队列类型,ThreadPoolExecutor则只是统一管理这个队列的读写访问。
// 任务存储队列
private final BlockingQueue<Runnable> workQueue;
// 将任务存入队列
private boolean addWorker(Runnable firstTask, boolean core) {
// 省略其他代码...
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 = runStateAtLeast(CHECK_IF_QUEUE);
if (rs < SHUTDOWN &&
(rs != QUEUE || (firstTask == null && !workQueue.isEmpty()))) {
if (core) {
// 创建核心线程
} else {
// 尝试创建非核心线程
}
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (!workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
上面的addWorker方法是向线程池提交任务时被调用的。在其中,线程池会先尝试将任务封装为一个Worker对象,然后再将其存入workQueue队列中。当然,如果有空闲线程,也可以直接分配给这个Worker执行。整个入队过程是通过ReentrantLock加锁实现的,以确保线程安全。
2、工作线程的生命周期
线程池中的每个工作线程都是ThreadPoolExecutor的一个内部类Worker的实例,它实现了Runnable接口。当Worker线程启动时,会不断地从workQueue取出任务执行,直到线程池被关闭。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
final Thread thread;
Runnable firstTask;
public void run() {
try {
Runnable task = firstTask;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
private Runnable getTask() {
for (;;) {
try {
// 从阻塞队列取出任务
return workQueue.take();
} catch (InterruptedException retry) {
// 中断处理
}
}
}
final void runTask(Runnable task) {
beforeExecute(thread, task);
try {
task.run();
} finally {
afterExecute(task);
}
}
}
如代码所示,Worker线程的主要工作就是不断地通过getTask方法从workQueue中取出任务,并运行runTask方法执行它们。getTask使用了BlockingQueue的take方法,如果队列为空,当前线程会被阻塞等待直到有新任务可取。在执行每个任务之前,都会调用ThreadPoolExecutor的beforeExecute钩子;执行完毕后,会调用afterExecute钩子,我们可以在其中实现自定义的统计、监控、异常处理等逻辑。
特别需要注意的是,一旦Worker线程从getTask返回null, 即workQueue已被置为终止状态,该线程的run方法就会结束并退出。如果我们希望线程能继续保持存活以等待新的任务到来,就必须在合适的时机向workQueue重新注入"Terminal"任务,防止工作线程过早退出。
除此之外,我们还可以看到Worker继承自AbstractQueuedSynchronizer。这是JDK并发包中用于构建同步组件的基础框架,它为Worker提供了一些用于同步状态维护和管理的工具方法,比如占有释放等。不过这些同步状态控制对Worker的使用者来说是透明的。
3、线程回收与扩容机制
线程池在运行过程中,需要时刻关注线程的数量变化,根据任务的实际负载来调整工作线程的数目,以达到资源利用最大化。这个动态调整过程涉及到线程的扩容和回收机制。
对于核心线程和非核心线程,ThreadPoolExecutor采取了不同的处理策略:
- 核心线程数量固定,一旦创建就不会被销毁,会一直存活在线程池中等待新任务到来。
- 非核心线程数量则是弹性的,如果一个非核心线程在keepAliveTime时长内都一直空闲,那么它会被标记为可被回收,最终会被自动销毁。
我们来看一下核心线程如何保持存活的:
private boolean removeWorker(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 如果当前线程是核心线程,则不能移除
if (runStateLessThan(STOP) &&
!completedAbruptly &&
w != null &&
w.getState() != TERMINATED &&
workers.remove(w)) {
// 根据allowCoreThreadTimeOut参数来决定核心线程是否保留存活
boolean reservedCoreThread = allowCoreThreadTimeOut ? null : this.corePoolSize;
if (reservedCoreThread == 0)
interruptIdleWorkers(ONLY_ONE);
else
interruptIdleWorkers(EXCEPTION_KEEPER);
return true;
}
} finally {
mainLock.unlock();
}
return false;
}
这段代码中的interruptIdleWorkers方法就是用来中断空闲的工作线程,并判断它们是否可以被回收的关键所在。
4、非核心线程的回收
对于非核心线程,ThreadPoolExecutor会根据keepAliveTime参数来判断线程是否应该被回收。如果一个线程在指定的keepAliveTime时长内一直保持空闲状态,那么该线程就会被标记为将被终止(TIDYING)状态,随后会被最终回收。
private void interruptIdleWorkers(BlockingQueue<Runnable> queue) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
//如果线程存在且是空闲状态
if (!t.isInterrupted() && w.tryLock()) {
try {
//根据线程池状态和线程类型决定是否中断该线程
if (shouldInterruptWorker(w)) {
w.interruptNow();
interruptedWorkers.incrementAndGet();
}
} finally {
w.unlock();
}
}
}
} finally {
mainLock.unlock();
}
}
private boolean shouldInterruptWorker(Worker w) {
//如果keepAliveTime=0,非核心线程直接中断
//否则,如果该线程空闲时间超过了keepAliveTime,也应中断
return w.poolState() == TERMINATED ||
(allowCoreThreadTimeOut && w.isCore())
|| (w.idleTimeout >= this.keepAliveTime);
}
从上面的代码可以看出,ThreadPoolExecutor会遍历线程池中的所有工作线程,如果发现有非核心线程的空闲时长超过了keepAliveTime,那么就会中断并将其标记为中断状态。
当然,仅仅中断线程还不够,我们还需要一个机制去移除和回收这些多余的线程。这就是getTask方法的作用了:
private Runnable getTask() {
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
for (;;) {
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
// 省略其他代码...
} catch (InterruptedException ie) {
// 如果发生中断,那么当前线程需要被移除回收
wt.interrupt();
try {
throw new RejectedExecutionException("...");
} catch (RejectedExecutionException ree) {
return handlerCaughtException();
}
}
}
}
在getTask中,如果当前线程是非核心线程,那么取任务时它就只能调用workQueue的poll方法而不是take。这个poll方法会阻塞至多keepAliveTime指定的时间。
一旦超时还没取到任务,poll就会返回null。这时getTask方法就会捕获一个InterruptedException中断异常,从而触发当前线程的移除回收逻辑。
这样通过keepAliveTime超时控制和getTask中断检测的双重机制,ThreadPoolExecutor就实现了自动化的非核心线程回收。
5、线程扩容机制
前面我们重点分析了线程回收的逻辑,相比之下,线程扩容的过程则相对简单一些。当有新任务进来时,如果线程池中无空闲线程,且运行线程数还未达到maximumPoolSize的上限,ThreadPoolExecutor就会创建新的工作线程来执行这个任务。
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
...
if (runStateAtLeast(SHUTDOWN)) {
return false;
}
for (;;) {
int c = ctl.get();
int rs = runStateFrom(c);
// 如果线程数量已经超过最大限制,直接返回false
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();
if (runStateAtLeast(SHUTDOWN))
continue retry;
else
continue;
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
...
workerAdded = true;
} finally {
...
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
return workerStarted;
}
这个addWorker方法是向线程池提交新任务时被调用的关键入口。在方法内部,它会首先判断线程池是否还运行正常,以及当前线程数是否已经达到了最大上限。
如果线程数量还没有达到上限,就会通过对CAS自旋锁的操作,尝试将workerCount值+1,然后创建新的工作线程并启动它。整个过程是一个完整的CAS自旋操作,通过不断重试直到成功给workerCount完成原子递增。
值得一提的是,这里addWorker方法在创建新线程时,会区分是创建核心线程还是非核心线程。如果是核心线程,只要线程数还没到corePoolSize上限就允许创建;如果是非核心线程,只有在线程总数没超过maximumPoolSize时才会被创建。这个核心与非核心线程的区分,是线程池实现灵活伸缩的关键所在。
通过上面的线程回收与扩容机制的分析,我们可以看出ThreadPoolExecutor的线程动态调节过程是如何实现资源利用最大化的。它让核心线程一直保留,除非线程池被shutdown。
6、拒绝策略
虽然线程池通过动态调节线程数量来最大化资源利用,但极端情况下,如果任务过多而无法继续扩容时,它必须采取一些策略来处理新的任务请求。ThreadPoolExecutor使用了一个内部接口RejectedExecutionHandler来封装不同的拒绝策略。
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
ThreadPoolExecutor自带的四种拒绝策略分别是:
- AbortPolicy:直接抛出RejectedExecutionException异常,默认策略
- CallerRunsPolicy: 用调用者所在的线程来运行被拒绝的任务
- DiscardPolicy: 直接丢弃被拒绝的任务
- DiscardOldestPolicy: 丢弃队列中最早的一个未执行的任务,并尝试提交新任务
我们可以根据具体的业务场景为线程池指定合适的拒绝策略。比如对于重要的系统核心流程,可以采用CallerRunsPolicy,确保任务至少可以在当前线程中得到执行;而对于不太重要的任务,如日志记录等,我们则可以选择直接丢弃。
7、线程工厂
线程工厂ThreadFactory是另一个可定制的组件,我们可以利用它来自定义创建线程的行为。比如为线程指定有意义的名字,设置优先级等。如果不指定,线程池会使用默认的线程工厂,创建普通非守护线程。
public interface ThreadFactory {
Thread newThread(Runnable r);
}
ThreadPoolExecutor中也提供了一个默认的线程工厂实现DefaultThreadFactory。我们可以通过重写ThreadFactory接口的newThread方法来自定义线程创建的细节。比如下面的实现就为每个线程指定了有意义的名字:
static class CustomThreadFactory implements ThreadFactory {
private AtomicInteger threadNumber = new AtomicInteger(1);
private String prefix;
public CustomThreadFactory(String prefix) {
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, prefix + "-thread-" + threadNumber.getAndIncrement());
return t;
}
}
通过线程工厂,我们还可以给每个线程设置一些有用的上下文信息,比如客户端标识、任务类型等,供我们在后续的扩展中使用。
通过上面的分析,我们可以看到ThreadPoolExecutor中还提供了许多可扩展的点,让我们可以最大限度地定制线程池的功能。合理利用这些扩展点,可以有效地提高系统的可靠性、可维护性,满足各种复杂场景下的需求。