线程池是如何执行任务的?

209 阅读9分钟

序言

线程池在日常工作当中用的非常多,其中包含七个核心参数,它包含了一组预先创建的线程,可以用于执行多个异步任务。线程池中的线程可以被重复利用,避免了为每个任务都创建和销毁线程所带来的开销。那线程池是如何执行我们传递给它的任务呢?

构造方法

我们来看一下线程池中参数最多的构造方法,另外两种构造方法都是调用这个构造方法,只是参数是线程池中的默认参数。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) 
                          
                          
                          
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler)


//  七个参数的构造方法
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

这里介绍一下每个参数的作用:

(1)int corePoolSize: 指定线程池中的核心线程数量(最少的线程个数),线程池中会维护一个最小的线程数量,即使这些线程处于空闲状态,它们也不会被销毁,除非设置了 allowCoreThreadTimeOut;默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程;在实际中如果需要线程池创建之后立即创建线程,可以通过以下两种方式:

boolean prestartCoreThread(),初始化一个核心线程;
int prestartAllCoreThreads(),初始化所有核心线程;

(2)int maximumPoolSize: 指定线程池中允许的最大线程数,当核心线程全部繁忙且任务队列存满之后,线程池会临时追加线程,直到总线程数达到 maximumPoolSize 这个上限;

(3)long keepAliveTime: 线程空闲超时时间,如果一个线程处于空闲状态,并且当前的线程数量大于 corePoolSize,那么在指定时间后,这个空闲线程会被销毁;

(4)TimeUnit unit keepAliveTime 的时间单位 (天、小时、分、秒......)

(5)BlockingQueue workQueue: 任务队列,当核心线程全部繁忙时,由 execute 方法提交的 Runnable 任务存放到该任务队列中,等待被核心线程来执行;该队列只会保存 execute 方法所提交的可运行任务,submit 方法提交的可运行任务不会被保存

image.png

(6)ThreadFactory threadFactory: 线程工厂,用于创建线程,一般采用默认的即可,也可以自定义实现,如果想要自定义实现,则需要去实现ThreadFactory接口,下面是 JDK 中两种内置的默认线程工厂

Executors.defaultThreadFactory()
Executors.privilegedThreadFactory()

(7)RejectedExecutionHandler handler: 拒绝策略(饱和策略),当任务太多来不及处理时,如何“拒绝”任务?任务拒绝是线程池的保护措施,当核心线程 corePoolSize 正在执行任务、线程池的任务队列workQueue 已满、并且线程池中的线程数达到 maximumPoolSize 时,就需要“拒绝”掉新提交过来的任务。JDK 内置以下四种拒绝策略:AbortPolicyCallerRunsPolicyDiscardOldestPolicyDiscardPolicy

  • AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常,这是线程池默认 的拒绝策略,在任务不能再提交的时候抛出异常,让开发人员及时知道程序运行状态,这样能 在系统不能承载更大的并发量时,及时通过异常信息发现;

  • DiscardPolicy:直接丢弃任务,不抛出异常,使用此策略可能会使我们无法发现系统的异 常状态,建议一些无关紧要的业务采用此策略;

  • DiscardOldestPolicy:丢弃任务队列中靠最前的任务,并执行当前任务,是否要采用此拒绝 策略,根据实际业务是否允许丢弃老任务来评估和衡量;

  • CallerRunsPolicy: 交由任务的调用线程(提交任务的线程)来执行当前任务;这种拒绝策 略会让所有任务都能得到执行,适合大量计算类型的任务执行,使用这种策略的最终目标是要 让每个任务都能执行完毕,而使用多线程执行计算任务只是作为增大吞吐量的手段;

除了上面的四种拒绝策略,还可以通过实现 RejectedExecutionHandler 接口,实现自定义的拒绝策略;

execute方法执行的原理

假如我们运行如下代码,线程池是如何执行这个任务的呢?

public static void main(String[] args) {

    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            5,
            10,
            10,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(1),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy()
    );


    // 核心线程处于空闲状态时,允许销毁核心线程,需要在线程池调用前设置,默认为false
    threadPoolExecutor.allowCoreThreadTimeOut(true);
    // 此方法可以直接创建所有的核心线程数量的线程
    threadPoolExecutor.prestartAllCoreThreads();


    threadPoolExecutor.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "线程被调用");
        }
    });

    // 关闭线程池
    threadPoolExecutor.shutdown();
//        System.out.println(Runtime.getRuntime().availableProcessors());
}

接下来我们来看 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);
}

ctl 是一个原子类,它经过精心巧妙的设计,可以用它一个值来表示当前线程池的状态和线程池中运行的线程数量两个值。 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));解释完这个变量后, 接下来,我们来一步一步的分析源代码。 首先看如下代码:

//workerCountOf方法获取控制变量ctl低29位的值,表示当前活动的线程数; 
//如果当前活动线程数小于核心线程数corePoolSize,则新建一个线程放入线程池中,并把任务添加到该线程中运行;
if (workerCountOf(c) < corePoolSize) { 
    if (addWorker(command, true)) 
        return; 
    c = ctl.get(); 
}

workerCountOf(c) 方法就是用来获取线程池中工作的线程数量,然后与核心线程数进行比较,然后就会进入到addWorker(command, true)方法中,代码如下:

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (int c = ctl.get();;) {
        // 检查线程的状态,如果是 SHUTDOWN 并且 状态是 STOP 或者 任务不为null和队列不为空。
        // 注意这里线程的状态并不是非此即彼的关系,在源码当中它是一种包含关系。
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            return false;

        for (;;) {
            // 判断线程数量,根据传入的core参数,如果为true,就与核心线程数进行比较,如果为false,就与非核心先吃数比较
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                return false;
            // 尝试增加线程池的工作线程数,如果增加成功,则跳出外层for循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            // 表示增加线程池的工作线程数失败,由于workerCount更改,CAS失败;重试内循环
            c = ctl.get();  // Re-read ctl
            if (runStateAtLeast(c, SHUTDOWN))
                continue retry;
        }
    }

    // Worker线程是否启动
    boolean workerStarted = false;
    // Worker线程是否添加
    boolean workerAdded = false;
    // Worker 是一个线程类,该类实现了Runnable接口的,我们传递过来的任务在线程池中都是以Worker对象的形式存在
    Worker w = null;
    try {
        // 根据firstTask来创建Worker对象
        w = new Worker(firstTask);
        // 每一个Worker对象都会创建一个线程
        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 c = ctl.get();
                // 检查线程池的运行状态
                if (isRunning(c) ||
                    (runStateLessThan(c, STOP) && firstTask == null)) {
                    // 如果当前线程的状态不是新建,则抛出IllegalThreadStateException异常
                    if (t.getState() != Thread.State.NEW)
                        throw new IllegalThreadStateException();
                    // 将该worker对象添加到set集合中,workers是一个HashSet
                    workers.add(w);
                    // 修改线程添加状态为true
                    workerAdded = true;
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // 调用start方法来启动先吃
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        // 如果Worker线程没有启动成功
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

到这里 addWorker()方法就分析完毕了,但是其中的 Worker 类是一个比较重要的类,上面只是简单提了一嘴,接下来,看一下Worker的源代码,我这里只截取了部分

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    /** Thread this worker is running in. Null if factory fails. */ 
    final Thread thread; 
    
    /** Initial task to run. Possibly null. */ 
    Runnable firstTask;
    


    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
    
    @Override public void run() { 
        runWorker(this); 
    }

由此可见,真正执行任务时,调用的是一个 runWorker()方法,其代码如下:

final void runWorker(Worker w) {
    // 拿到当前线程
    Thread wt = Thread.currentThread();
    // 获取任务
    Runnable task = w.firstTask;
    // 将 firstTask 置为 null
    w.firstTask = null;
    // 允许响应中断
    w.unlock(); // allow interrupts
    // 线程退出的原因,true是任务导致,false是线程正常退出
    boolean completedAbruptly = true;
    try {
        // 如果当前任务为空,或者当前任务队列为空,则停止循环,getTask()方法是去从任务队列中取任务进行执行
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // 如果线程池是stop状态,并且线程没有被中断,就要确保线程被中断,如果线程池不是,确保线程池没有被中断; 
            // 清除当前线程的中断标志,做一个recheck来应对shutdownNow方法;
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                // 执行前(空方法,由子类重写实现)
                beforeExecute(wt, task);
                try {
                    // 执行Runnable类的run()方法
                    task.run();
                    // 执行后(空方法,由子类重写实现)
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                task = null;
                // 线程池完成的任务数 +1
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

上面的代码已经介绍完成,我们来看如下代码:

// 如果线程池是运行状态,并且队列插入任务成功的话
if (isRunning(c) && workQueue.offer(command)) { 
    // 重新获取ctl值
    int recheck = ctl.get(); 
    // 再次判断线程池是否是运行状态,如果不是运行状态,由于之前已经把command添加到workQueue中了,此时需要移除该command;
    if (! isRunning(recheck) && remove(command)) 
        //线程池不是运行状态并且移除该command成功,则使用拒绝策略对该任务进行处理;
        reject(command); 
    // 获取线程池的线程工作数,如果为0,则调用addWorker方法创建一个线程
    else if (workerCountOf(recheck) == 0) 
        addWorker(null, false); 
}

至此为止,线程池中执行过程就分析完毕了,用下面一张图来表示:

image.png

线程池生命周期钩子

我们来分析上面的代码的时候,可以看到三个方法beforeExecuteafterExecuteterminatedterminated() 方法在 processWorkerExit() 中的tryTerminate()中 会调用。这些都是空方法,如果我们需要自定义线程池时,必须写一个线程池类去继承 ThreadPoolExecutor,那么就可以在自己的类中实现线程池的扩展行为。

beforeExecute、afterExecute 和 terminated 方法,可以在任务执行前、执行后和线程池关闭前执行一些代码来扩展线程池的行为;

总结

理解这个过程,大家需要debug模式下去一步一步调试,我这里是Jdk17的版本,其他的JDK版本的代码可以不能,但意思都是差不多的。最后就是这个线程池的生命周期钩子,虽然目前用的不多,但是如果在工作当中遇到,方便省事。