All Articles

AQS 常用实现类剖析之ThreadPoolExecutor

AQS 常用实现类剖析之 ThreadPoolExecutor

前言

本来想把实现类统一写一篇的,但是发现每个实现类其实都够让我喝一壶的,集中在一起有恐无法详尽其精髓,遂单独起了一篇来详细展开下ThreadPoolExecutor与AQS黑魔方的关系和一些细节。

ThreadPoolExecutor 介绍

ThreadPoolExecutor是JDK提供的线程池基础类,里面定义了线程池的一些核心属性(比如核心线程数、最大线程数、keepAlive时间、拒绝策略等),其内部包含了线程池下对线程的基本操作封装,方便开发者在其基础上快速地使用线程池技术。

关注点问题

  1. 核心线程在空闲时是什么状态,如何保证?
  2. 如何做到线程池的优雅停机的?
  3. 线程池的监控要怎么做?
  4. threadPoolExecutor和AQS是什么关系?
  5. 线程池内部的逻辑单元有哪些,流转流程是什么样的?
  6. 超过核心线程的线程什么时候回收?回收的时机/条件是什么?
  7. JDK的线程池写的好吗?哪里好?是否还有优化空间?

ThreadPoolExecutor 核心原理

ThreadPoolExecutor的核心处理逻辑无外乎围绕线程池启停与运行时线程操作相关进行的,所以下面将从这几个动作为出发点,先以图形的形式描绘处理流程,再结合代码深入剖析其内部实现,由面到点地去深入一窥线程池的内部运行原理。

图解

  1. 线程池启动 (无构造)

线程池任务执行原理图

  1. 线程池关闭 todo

源码详解

线程池的状态

  1. 线程池的状态表示字段:ctl

在源码中可以看到到处都有使用到ctl这个变量,其核心围绕的是它的两个状态表示,线程池状态和worker数量。

线程执行器 Worker

  • work基于AQS实现,从上面的流转图也可以看到内部执行线程的时候其实跑的是worker类,worker类会不断循环去队列中捞取任务,同时通过work基于AQS的lock、tryLock等操作来保证同步操作

线程池的生命周期

  • 通过ctl表明状态(RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED),修改的时候使用cas设置

开发性(beforeExecute、afterExecute)

  • 线程执行前后有定义before和after两个方法,可以做一些订制操作,比如监控等。
  • 线程池状态,原来线程池也有生命周期状态。
/*
     *   RUNNING:  Accept new tasks and process queued tasks
     *   SHUTDOWN: Don't accept new tasks, but process queued tasks
     *   STOP:     Don't accept new tasks, don't process queued tasks,
     *             and interrupt in-progress tasks
     *   TIDYING:  All tasks have terminated, workerCount is zero,
     *             the thread transitioning to state TIDYING
     *             will run the terminated() hook method
     *   TERMINATED: terminated() has completed
/

线程池发布任务的处理流程(源码)

线程池启动方法具体流程(启动线程,sumbit核心是execute,所以先看一下execute,方法处理流程如下) alt text

  1. 当少于核心线程数CorePoolSize的线程在运行时,则直接新建一个线程,调用addWorker(会自动执行runState和workerCount操作)
  2. 如果task成功加入队列(workQueue》offer),还需要double-check下线程池是否正在关闭,如果是得移出队列并出发拒绝规则,如果成功入队后就完事了,后续worker异步执行的时候会从队列中捞任务。
  3. 如果入队失败,则试着加入到非核心线程池中,如果还失败,则触发拒绝策略。
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);
    }

拒绝请求:依赖拒绝策略处理器RejectedExecutionHandler来处理线程

final void reject(Runnable command) {
    handler.rejectedExecution(command, this);
}

回顾问题

  1. 核心线程在空闲时是什么状态,如何保证

核心线程会因为从阻塞队列中获取不到资源而保持阻塞状态
错了,如果拿不到资源超时就会退出了,如果大于coreSize会释放掉线程。

  1. 如何做到线程池的优雅停机的?

    • 大致原理是依赖mainLock和 AQS的condition类,condition可以通知其他线程关闭
  2. 线程池的监控要怎么做?- to看源码

通过before和after可以做一些动作。

  1. threadPoolExecutor和AQS是什么关系?
  • 里面的worker继承了AQS,内部使用的ReentrantLock也是AQS的实现。
  • 在worker执行的过程使用了worker的lock操作来保障多线程同步操作。同时依赖aqs中包装的condition对象来做优雅停机。
  1. 线程池内部的逻辑单元有哪些,流转流程是什么样的?
    • worker,workerQueue,ThreadFactory,RejectHandler,mainLock,runtimePermission,
    • 通过执行器worker以及worker队列来实现了
  2. 超过核心线程的线程什么时候回收?回收的时机/条件是什么?

没细看到这个,有一个decrement逻辑,可能是这个地方

  1. JDK的线程池写的好吗?哪里好?是否还有优化空间

那必须是比我写的好,抽象的逻辑概念也挺好的,不过还是挺难看懂的,涉及到的双属性整型ctl、AQS、mainLock操作都是很硬核的概念,尤其是worker里面的哪个thread包装,如果看漏了容易把人整懵,容易以为直接把原生的Runnable丢给了底层调用,最后发现线程运行的还是执行器worker。

总结

【学习价值】ThreadPoolExecutor内部使用了很多锁,其原理也可作为并发编程的学习范本,非常具备学习价值。
【原理的作用】Threadlocal中涉及的概念比较多,了解其worker、队列进出逻辑对性能优化和问题排查都十分必要。

Published Jul 9, 2024

Software Engineer from China, currently more attention to the automation field of ai interaction