【JUC】线程池的源码理解

引言
线程池是一种基于池化思想对并发任务进行管理的技术。在并发环境下,并发任务一方面持续产生,另一方面在某一时刻不能确定并发任务的数量,这就会导致频繁创建销毁线程资源、当并发任务过多时产生系统资源压力和线程调度压力。
通过将线程与任务解耦,线程池实现了线程复用,因此有以下好处:
1.通过复用线程,减少线程创建与销毁的开销,
2.提交任务时,如果线程池中存在空闲线程可以实现快速响应,当任务过多时既可以避免消耗过多线程资源又可以减少调度的压力,
3.提高对并发任务执行的可管理性,通过管理线程池中的线程实现对任务执行的控制。
线程池的运行分为任务管理与线程管理两部分,二者是生产者消费者关系。
线程池的生命周期
JUC的ThreadPoolExecutor线程池通过一个变量维护两方面信息private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));。它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内存活线程的数量 (workerCount),高3位保存runState,低29位保存workerCount。用一个变量去存储两个值,可避免在并发场景中出现不一致的情况,对于需要同时获取线程池运行状态和线程数量的情况,避免使用锁。

线程池生命周期
RUNNING:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
SHUTDOWN:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务,会中断所有空闲线程。
STOP:线程池处在STOP状态时,不接收新任务,也不处理已添加的任务,并且会中断所有线程。
TIDYING:当执行完shutdownshutdownnow方法后,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。
TERMINATED:执行完terminated()后,线程池彻底终止,就变成TERMINATED状态。
任务管理
当一个任务被提交后,线程池检查当前的运行状态、存活线程数、运行策略,决定接下来是创建新线程执行,或是添加到队列中等待被执行,亦或是执行拒绝策略。
1.任务提交
可以通过两个方法提交任务,execute()submit()submit内部最终调用的是execute,在调用之前会将传入的RunnableCallable任务封装成RunnableFuture并返回给任务提交线程,提交线程得到返回后可以调用getisDone方法检查结果。在返回的RunnableFuture实现类中有一个private Object outcome;字段,如果任务正常执行其中放置返回的结果,如果出现异常则放置异常对象。使用RunnableFuture实现类访问任务结果存在阻塞问题,并且不利于任务的组合编排,因此更好的方式是CompletableFuture,具体查看参考一与参考二。

// java.util.concurrent.AbstractExecutorService.submit()
public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}
// java.util.concurrent.ThreadPoolExecutor.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);
}

如果当前存活线程数小于核心线程数if (workerCountOf(c) < corePoolSize),直接调用addWorker方法创建一个新线程,将当前任务作为firstTask执行。从中可以看出,即使当前核心线程数没满并且有空闲线程在等待任务,线程池也会直接创建新线程。这么做的原因我想有两点:一是核心线程数应该是需要被快速满足的,二是如果将其交于空闲线程则需要先放到任务队列中,如果当前存活线程数量较少并且同时有较多任务提交,那么可能造成响应迟钝。
如果当前线程数已经达到了核心线程数if (isRunning(c) && workQueue.offer(command)),那么会调用workQueue.offer(command)尝试将任务添加到任务队列中。在一般的offer实现中,只要任务队列还有容量就会将任务放入其中,这种做法会导致只有任务队列已经满了才会创建新线程。对于要处理的任务属于IO密集型的情况,可以重写offer方法,检查当前线程数与最大线程数,实现任务处理逻辑的修改。
如果任务没有被加到任务队列中else if (!addWorker(command, false)),继续调用addWorker方法创建一个新线程,将当前任务作为firstTask执行。如果创建失败说明当前线程数已经达到最大或者当前线程池已经关闭,执行拒绝策略。
2.任务调度
任务调度涉及两个部分:执行任务的Worker和任务队列。
addWorker中会创建一个Worker实例,其中一个字段保存Thread实例,而Worker实现了Runnable接口被传入Thread的构造器,因此线程运行实际上调用的是Worker的Run方法。

// java.util.concurrent.ThreadPoolExecutor.Worker.run()
public void run() {
    runWorker(this);
}
// java.util.concurrent.ThreadPoolExecutor.runWorker()
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);
    }
}

在任务执行过程中,实际调用的是任务提交时Runnablerun()Callablecall().如果在执行过程中出现了异常可以分为两种情况。第一,如果是通过submit提交任务,那么被放入任务队列中的实际上是RunnableFuture实现类实例,其中对run方法进行了全throwable的捕获,捕获到的异常被放入outcome字段,因此任务执行出现异常后线程会继续存活,并尝试从任务队列中获取新任务。第二,如果是通过execute提交任务,那么被放入任务队列中的实际上是Runnable实现类实例,那么出现异常时就会像一般的线程发生异常时的处理流程一样。对于一般的线程,无论是字节码检查抛出异常还是jvm处理信号后抛出异常,线程会在线程栈中捕获异常,如果未捕获,那么会终止线程,并且调用当前线程或者当前线程组或者当前jvm的未捕获处理函数。因此在这种方式中,线程一定会终止,异常也可能无法得到通知与处理。
在线程终止后,会调用processWorkerExit方法,其中会根据completedAbruptly,如果线程池状态与存活线程数量满足条件,则会新建一个没有初始任务的线程addWorker(null, false);
任务队列则实现BlockingQueue接口,不同的实现可以执行不同的任务存取策略,如有界容量、优先级排序、延迟获取、同步存取等。
线程管理
1.存活线程管理
核心线程数是想要快速达到的数量。这些数量的核心线程是线程池执行任务的中坚力量,在默认实现中是懒加载的,但是通过调用prestartCoreThread启动一个线程或prestartAllCoreThreads启动核心线程数的线程。核心线程数默认是一直满足的,通过allowCoreThreadTimeOut设置可以允许线程池中的存活线程数量少于核心线程数。
线程池其实想要的只是核心线程数个线程,但是允许线程池最多拥有最大线程数的线程来预防突发状况。当线程数大于核心数之后,如果线程空闲了一段时间(KeepAliveTime),就回收线程。
线程池数量以及其他参数往往需要动态调整,运行期间可以调用以下方法动态调整线程池的参数:
setCorePoolSize():动态调整线程池核心线程数,
setMaximumPoolSize():动态调整线程池最大线程数,
setKeepAliveTime(): 空闲线程存活时间,
allowsCoreThreadTimeOut():允许存活线程数小于核心线程数。
2.线程复用
在上面java.util.concurrent.ThreadPoolExecutor.runWorker()可以看到,在线程正常执行完一个任务后,会进入下一轮循环while (task != null || (task = getTask()) != null),此处从任务队列中获取下一个任务。

// java.util.concurrent.ThreadPoolExecutor.getTask()
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;
        }
    }
}

这里核心代码就是从任务队列中取任务,采用poll还是take取决于allowCoreThreadTimeOut和当前存活的线程数量。如果使用take则表明没有取到任务当前线程就会被阻塞挂起,直到有了新的任务才会被唤醒。如果使用poll则表明没有按时取到任务那么会进入下一轮循环for (;;),那么如果当前线程数超过核心线程数就会返回null,接着在runWorker()中就会退出循环while (task != null || (task = getTask()) != null),然后调用processWorkerExit完成线程退出。

参考一:https://blog.51cto.com/u_14014612/13013310
参考二:https://tech.meituan.com/2022/05/12/principles-and-practices-of-completablefuture.html
参考三:https://www.cnblogs.com/wang-meng/p/12945703.html

posted @ 2025-07-19 19:56  hzx1011  阅读(12)  评论(0)    收藏  举报