深入理解ThreadPoolExecutor线程池工作原理源码解析

0. 前言

在这里插入图片描述

背景:最近技术交流群里有个新同学,面试的时候被问到线程池相关的问题,答的不是很好,群里针对线程池的问题,也有激烈的讨论。所以借此写一个简单的源码解析,帮助大家进行记忆学习,知其然知其所以然。

我们先来了解一下线程池的本质,其实就是为了复用多线程场景下,线程创建销毁过程中的资源开销。而利用池化技术设计的一套线程池模型,就和我们常见的连接池、对象池思想类似,主要进行资源复用,减少资源开销。所以线程池模型的本质是对任务和线程的管理,让干活的和活进行分离。将资源合理的进行复用,所以最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。为了解决解耦问题,因此线程池中就是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

所以基于以上的思想我们来从两方面分析源码

  1. 线程的生命周期管理
  2. 任务的调度管理

1. 生命周期管理

首先通过源码我们可以发现Java中的线程池实现是通过Executor、ExecutorService、AbstractExecutorService和ThreadPoolExecutor这些接口和类来完成的。这些接口和类提供了一种任务提交和任务执行进行解耦的思想,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力,比如扩充执行任务的能力和提供了管控线程池的方法。AbstractExecutorService将执行任务的流程串联起来,保证下层的实现只需关注一个执行任务的方法。最底层的实现类ThreadPoolExecutor则实现了最复杂的运行部分,它维护自身的生命周期,同时管理线程和任务,实现并行任务的执行。

所以由此可见ThreadPoolExecutor基本上所有的线程池相关操作都离不开它,它提供了一些非常丰富的线程池管理方法,并且还支持线程池中的线程复用、队列阻塞等功能,可以帮助我们开发人员更加方便快捷地管理线程。那么本章节我们就以ThreadPoolExecutor 的源码解析来了解线程池的工作原理。我们分别通过线程池的生命周期来分析源码。
在这里插入图片描述

1.1 创建

其实我们最常见的Executors.new****Pool() 来创建线程池,但是我们只要点进去过源码就会发现这个只是一个皮,最后都是使用的ThreadPoolExecutor构造方法来实现创建线程池。其实在工作中,我们大多数都是自定义线程池,因为这个工具类生成的如下线程池都有各自的弊端,甚至有的线程池会造成OOM。使得整个服务宕掉。我们可以粗略了解一下。如下

  1. newFixedThreadPool(int nThreads):创建一个固定大小的线程池,该线程池中的线程数量固定不变,当有新的任务提交时,若线程池中有空闲线程,则立即执行该任务,否则将任务加入等待队列中。

  2. newCachedThreadPool():创建一个可缓存的线程池,该线程池中的线程数量不固定,当有新的任务提交时,若线程池中有空闲线程,则立即执行该任务,否则创建新的线程来执行该任务,当线程空闲一定时间后,将被回收。

  3. newSingleThreadExecutor():创建一个单线程的线程池,该线程池中只有一个线程在工作,所有任务都将排队执行,确保所有任务按照指定顺序执行。

  4. newScheduledThreadPool(int corePoolSize):创建一个定时任务的线程池,该线程池中的线程数量固定不变,可以延迟或定时执行任务。

那么下面我们对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:线程池中非核心线程的空闲存活时间;
  • unit:存活时间的单位;
  • workQueue:任务队列,用于存放等待执行的任务;也叫做任务缓冲模块。
  • threadFactory:线程工厂,用于创建新的线程;
  • handler:拒绝策略,当任务无法被执行时的处理方式。
  1. 核心线程(corePoolSize)数指的是线程池一开始就创建的线程数量,最大线程数则是线程池中最多可以创建的线程数量。当线程池中的线程数量超过核心线程数时,新的任务会被放到任务队列中;当任务队列已满时,才会创建新的线程,直到线程数量达到最大线程数(maximumPoolSize)为止,所以此处我们可以了解到,最大线程数,并不是核心线程数的补充,只是一个兜底的策略。所以后面我们看下源码解析。

  2. 那我们再聊一下线程池中的线程复用。首先线程复用的第一任务是要先有线程,那线程优势从哪儿来的,那么上面构造方法的参数中线程工厂(threadFactory)参数就是干这个事情,但一般情况下都是使用的默认的。自定义线程工厂是可以在创建线程时对线程进行一些额外的设置,如设置线程名称、设置线程为守护线程等。

  3. 拒绝策略(handler)用于处理无法被执行的任务,当任务队列已满并且线程数量已经达到最大线程数时,新的任务就无法被执行。拒绝策略可以选择抛出异常,或者在当前线程中执行任务,或者将任务加入到任务队列中等待下一次执行。

拒绝策略名称 描述
ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常。这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
ThreadPoolExecutor.DiscardPolicy 这种是最坑的,大家不要用这种,出了问题两眼一抹黑,坑人坑己。丢弃任务也不抛出异常。使用此策略,使我们无法发现系统的异常状态。
ThreadPoolExecutor.DiscardOldestPolicy 丢弃队列最前面的任务,然后重新提交被拒绝的任务。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
ThreadPoolExecutor.CallerRunsPolicy 由调用线程(提交任务的线程)处理该任务。这种情况是需要让所有任务都执行完毕,也就是你的任务特别重要,重要到阻塞后让主线程去干活的地步,但是这种的需要输出错误日志,触发报警,告知开发去优化代码, 在这种情况多线程仅仅是增大吞吐量的手段,最终必须要让每个任务都执行完毕。
  1. 任务队列(workQueue)的实现也有很多种,根据实际情况配置
    在这里插入图片描述

其他2个参数我就不讲了,作用基本上在源码解析中可以提现。

1.2 执行

ThreadPoolExecutor的execute方法:

1.2.1 任务执行入口


public void execute(Runnable command) {
    
    
// 检查任务是否为null,如果是则抛出 
    if (command == null)
        throw new NullPointerException();

    // 获取线程池的控制状态,即一个int类型的整数
    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); // addWorker 这个我们后面讲详细讲
    }
    // 如果无法将任务添加到队列中,创建一个新的非核心线程来执行该任务
    // addWorker 这个我们后面讲详细讲
    else if (!addWorker(command, false))
        reject(command);
}

execute向线程池中提交一个任务,其实还有一个方法,我们此处不讲了。当线程池中的线程数量小于核心线程数时,会直接创建一个新的线程来执行任务;当线程池中的线程数量已经达到核心线程数时,任务会被放入任务队列中等待执行。
当任务队列已满时,会根据当前线程池状态和拒绝策略来决定如何处理无法执行的任务。具体来说,如果线程池处于运行状态,并且任务可以被成功加入到任务队列中,那么就会返回;如果线程池已经关闭或者任务无法加入到任务队列中,就会执行拒绝策略。

1.2.2 addWorker解析

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;
            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 简单翻译一下  CAS操作失败,说明线程池中的工作线程数发生了变化,需要重新进行判断
        }
    }

    // 创建新的工作线程
    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); // 将新的工作线程添加到workers列表中
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
    
    
                mainLock.unlock(); // 释放线程池的主锁
            }
            if (workerAdded) {
    
    
                t.start(); // 启动新的工作线程
                workerStarted = true;
            }
        }
    } finally {
    
    
        if (! workerStarted) 
        // 如果启动新的工作线程失败,则进行相应的处理,此处如果感兴趣,可以点进去看下
        // 哈哈可以看到在线程池的实现中到处都是锁,
         // 顺便也可以了解到ReentrantLock原来用的地方这么多
          // 这也是多线程带来的问题,线程安全是所有实现多线程必须要考虑的事情
            addWorkerFailed(w);
    }
    return workerStarted; // 返回是否成功启动新的工作线程
}

addWorker方法用于向线程池中添加一个新的线程,并且返回一个boolean类型的值,表示线程是否启动成功。当线程池中的线程数量已经达到最大线程数时,或者线程池已经关闭时,就无法再添加新的线程了。

当新的线程启动成功时,会将线程加入到workers集合中,同时更新线程池的最大线程数。如果线程启动失败,则会执行addWorkerFailed方法,将添加线程的操作回退。
从网上找了个图,方便大家理解
在这里插入图片描述

1.2.3 Worker类解析

Worker类有很多。大家在看源码的时候切记注意是在java.util.concurrent 包下,不然牛头不对马嘴就尴尬了。

/**
 * Worker类继承了AbstractQueuedSynchronizer类,实现了Runnable接口。
 * 它是线程池中的一个工作线程,负责执行任务队列中的任务。
 */
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    
    
    /**
     * 这个类永远不会被序列化,但我们提供一个serialVersionUID来压制javac警告。
     */
    private static final long serialVersionUID = 6138294804551838833L;
    /** 这个工作线程正在运行的线程。如果工厂失败,则为null。 */
    final Thread thread;
    /** 要运行的初始任务。可能为null。 */
    Runnable firstTask;
    /** 每个线程的任务计数器 */
    volatile long completedTasks;

    /**
     * 使用给定的第一个任务和来自ThreadFactory的线程创建Worker。
     * @param firstTask 第一个任务(如果没有则为null)
     */
    Worker(Runnable firstTask) {
    
    
        // 在运行runWorker之前,禁止中断
        setState(-1);
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    /** 将主运行循环委托给外部的runWorker */
    public void run() {
    
    
        runWorker(this); 
        // == 这个源码我没有贴出来,但是也很关键,我找了个图,帮大家理解 == 
    }

    // 锁定方法
    //
    // 值0表示未锁定状态。
    // 值1表示已锁定状态。

    protected boolean isHeldExclusively() {
    
    
        return getState() != 0;
    }

    protected boolean tryAcquire(int unused) {
    
    
        if (compareAndSetState(0, 1)) {
    
     // 尝试获取锁
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int unused) {
    
    
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock()        {
    
     acquire(1); }
    public boolean tryLock()  {
    
     return tryAcquire(1); }
    public void unlock()      {
    
     release(1); }
    public boolean isLocked() {
    
     return isHeldExclusively(); }

    /**
     * 如果工作线程已经启动,则中断它。
     */
    void interruptIfStarted() {
    
    
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
    
    
            try {
    
    
                t.interrupt();
            } catch (SecurityException ignore) {
    
    
            }
        }
    }
}
  1. 我们可以看到Worker 类实现了Runnable接口,并且继承自AbstractQueuedSynchronizer类。Worker类中的run方法会不断地从任务队列中取出任务并执行,直到线程池关闭为止。
  2. 在Worker类中,我们主要关注tryAcquire和tryRelease方法。tryAcquire方法用于获取独占锁,当独占锁已经被其他线程占用时,tryAcquire方法返回false;tryRelease方法用于释放独占锁。
  3. 在runWorker方法中,当任务队列中已经没有可执行的任务时,线程会执行workerDone方法,将当前线程从workers集合中移除,并且尝试关闭线程池。
  4. isHeldExclusively()、tryAcquire()、tryRelease()、lock()、tryLock()、unlock()和isLocked()这些方法是Worker实现的AbstractQueuedSynchronizer抽象类中的一些锁定方法,此处就不解析了应该不影响大家整体理解。

runWorker(this); 方法实现虽然没有贴出来源码,但从网上找了个图帮大家理解一下
在这里插入图片描述

1.3 关闭

  1. 当线程池收到关闭命令后,线程池的状态会被设置为SHUTDOWN,此时线程池不再接受新的任务。接着,线程池会遍历任务队列中的任务,并将它们逐个取出交给线程池中的线程去执行。
  2. 在执行任务的过程中,如果线程池中的线程被中断,那么它们会抛出InterruptedException异常,此时需要将该异常抛出到任务的调用者处进行处理。
  3. 当任务队列中的任务执行完毕后,线程池中的线程会被逐个关闭,直到所有线程都关闭为止。在关闭线程池的过程中,如果任务队列中还有未执行的任务,那么这些任务将会被丢弃。
// shutdown方法,关闭线程池
public void shutdown() {
    
    
    // 获取线程池的锁
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        // 检查是否有关闭线程池的权限
        checkShutdownAccess();
        // 将线程池的运行状态设置为SHUTDOWN,不再接受新任务
        advanceRunState(SHUTDOWN);
        // 中断处于空闲状态的工作线程
        interruptIdleWorkers();
        // 调用onShutdown方法,用于ScheduledThreadPoolExecutor
        onShutdown();
    } finally {
    
    
        // 释放线程池的锁
        mainLock.unlock();
    }
    // 尝试终止线程池
    tryTerminate();
}

// interruptIdleWorkers方法,中断处于空闲状态的工作线程
private void interruptIdleWorkers() {
    
    
    // 获取线程池的锁
    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();
                }
            }
        }
    } finally {
    
    
        // 释放线程池的锁
        mainLock.unlock();
    }
}

// tryTerminate方法,尝试终止线程池
protected void tryTerminate() {
    
    
    // 循环判断线程池是否可以被终止
    for (;; ) {
    
    
        int c = ctl.get();
        // 若线程池仍在运行,或运行状态为TIDYING及以上,或者线程池未停止且任务队列非空,则不可以终止
        if (isRunning(c) || 
            runStateLessThan(c, TIDYING) ||
            (runStateAtLeast(c, STOP) && !workQueue.isEmpty()))
            return;
        // 若线程池中仍有工作线程,中断处于空闲状态的工作线程
        if (workerCountOf(c) != 0) {
    
     
            interruptIdleWorkers();
            return;
        }
        // 获取线程池的锁
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            // 如果线程池的运行状态为TIDYING,就执行terminated方法,更新线程池的状态
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
    
    
                try {
    
    
                    terminated();
                } finally {
    
    
                    // 将线程池的运行状态设置为TERMINATED,唤醒所有等待线程
                    ctl.set(ctlOf(TERMINATED, 0));
                    termination.signalAll();
                }
                return;
            }
        } finally {
    
    
            // 释放线程池的锁
            mainLock.unlock();
        }
    }
}

这块有个疑问,大家有手动调用过关闭线程池方法吗,基本上并没有,那么线程池关闭方法是什么时候调用的呢,如果有细心的同学,可能在日志中发现过,线程池关闭,一般是 Java 应用程序退出时,线程池会被自动关闭。这是因为线程池是通过线程来实现的,而线程是守护线程,当 Java 应用程序退出时,守护线程也会随之退出。我们也可以利用spring boot 的钩子方法或者注解手动调用。以确保在应用程序停止时所有任务都能够被正确地执行完毕。== 切记如果是kill -9 结束的的java应用,并不会走这块逻辑==

  @PreDestroy
    public void destroy() {
    
    
        executorService.shutdown();
    }

1.4 终止阶段

线程池中的所有线程都已经关闭,线程池对象被销毁,整个线程池的生命周期结束,此处没啥讲的。

2. 总结

到此我们把线程池工作原理的线程池的生命周期基本上讲解完成了。大家对线程池的创建,执行,关闭,销毁都有了一个简单的认识。其实线程池的源码涉及到了很多知识点,比如队列,线程,锁等等。我们看到了AtomicIntegerReentrantLockConditionvolatileAQS的使用。这些都是你面试时候的加分项,大家切记活学活用。学以致用,知其然知其所以然。由于篇幅和精力有限,关于线程池的另一部分任务的调度管理,我们下次再见。

猜你喜欢

转载自blog.csdn.net/wangshuai6707/article/details/131422670