并发系列——JUC高级篇(七)线程池原理

 

一、为什么要使用线程池

使用线程池主要有以下三个原因:

  1. 创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程。(线程复用)

  2. 控制并发的数量。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。(主要原因)(拒绝策略)

  3. 可以对线程做统一管理。(比如:延时执行、定时循环执行的策略等运用线程池都能进行很好的实现)

 

二、实现原理

线程池的实现是依据于Executor顶级接口,而ThreadPoolExecutor是这个接口的实现类。

Executor :不是线程池,是线程池的底层具体应用,线程池底层处理机制。线程池顶级接口。

线程池一般底层都是用ThreadPoolExecutor(除ForkJoinPool)实现

下面则直接看实现类ThreadPoolExecutor:

类图结构:

如图所示,Executors是个工具类(用来提供已经实现了的线程池),用来提供不同特性的线程池。ThreadPoolExecutor中的ctl是一个原子变量,用来记录线程池状态和线程池中的线程个数,类似于ReentrantReadWriteLock中使用一个变量来保存两种信息。

 

 

以下为与ctl相关的变量与函数:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 假设Integer为32位(不同平台下可能不同),则前3位用来表示线程运行状态,
// 后29位用来表示线程个数
private static final int COUNT_BITS = Integer.SIZE - 3;
// 00011111111111111111111111111111
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// 11100000000000000000000000000000
private static final int RUNNING    = -1 << COUNT_BITS;
// 00000000000000000000000000000000
private static final int SHUTDOWN   =  0 << COUNT_BITS;
// 00100000000000000000000000000000
private static final int STOP       =  1 << COUNT_BITS;
// 01000000000000000000000000000000
private static final int TIDYING    =  2 << COUNT_BITS;
// 01100000000000000000000000000000
private static final int TERMINATED =  3 << COUNT_BITS;

// 取高3位的值
private static int runStateOf(int c)     { return c & ~CAPACITY; }
// 低29位的值
private static int workerCountOf(int c)  { return c & CAPACITY; }
// 通过指定的rs(Running State)和wc(Workers Count)生成新的ctl状态值
private static int ctlOf(int rs, int wc) { return rs | wc; }

 

 

 

线程池的状态含义如下:

  • RUNNING:接受新任务并处理阻塞队列里的任务。
  • SHUTDOWN:拒绝新任务但是处理阻塞队列里面的任务。
  • STOP:拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务。
  • TIDYING:所有任务都执行完后当前线程池活动线程数为0,将要调用terminated方法(相当于一个过渡状态)。
  • TERMINATED: 终止状态,terminated方法调用完成后的状态。

看看ThreadPoolExecutor提供的四个构造函数:

// 五个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)

// 六个参数的构造函数-1
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory)

// 六个参数的构造函数-2
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)

线程池参数如下:

涉及到5~7个参数,我们先看看必须的5个参数是什么意思:

  • int corePoolSize:该线程池中核心线程数最大值

    核心线程:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干(铁饭碗),而非核心线程如果长时间的闲置,就会被销毁(临时工)。

  • int maximumPoolSize:该线程池中线程总数最大值

    该值等于核心线程数量 + 非核心线程数量。

  • long keepAliveTime非核心线程闲置超时时长

    非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则会也作用于核心线程。

  • TimeUnit unit:keepAliveTime的单位。

        TimeUnit是一个枚举类型 ,包括以下属性:

NANOSECONDS : 1微毫秒 = 1微秒 / 1000 MICROSECONDS : 1微秒 = 1毫秒 / 1000 MILLISECONDS : 1毫秒 = 1秒 /1000 SECONDS : 秒 MINUTES : 分 HOURS : 小时 DAYS : 天

  • BlockingQueue workQueue:阻塞队列,维护着等待执行的Runnable任务对象

    常用的几个阻塞队列:

    1. LinkedBlockingQueue

      链式阻塞队列,底层数据结构是链表,默认大小是Integer.MAX_VALUE,也可以指定大小。

    2. ArrayBlockingQueue

      数组阻塞队列,底层数据结构是数组,需要指定队列的大小。

    3. SynchronousQueue

      同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。

    4. DelayQueue

      延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。

在上一章中重点介绍各种阻塞队列

好了,介绍完5个必须的参数之后,还有两个非必须的参数。

  • ThreadFactory threadFactory

    创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级等。如果不指定,会新建一个默认的线程工厂。

static class DefaultThreadFactory implements ThreadFactory {
    // 省略属性
    // 构造函数
    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
        Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
            poolNumber.getAndIncrement() +
            "-thread-";
    }

    // 省略
}
  • RejectedExecutionHandler handler

    拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略,四种拒绝处理的策略为 :

    1. ThreadPoolExecutor.AbortPolicy默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。

    2. ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。

    3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。

    4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

其他参数:

    如上 ThreadPoo I Executor 类图所示,其中 mainLock 是独占锁, 用来控制新增 Worker 线程操作的原子性。 termination 是该锁对应的条件队列 , 在线程调用 awaitTermination 时 用来存放阻塞的线程。
    Worker 继承 AQS 和 Runnable 接口 , 是具体承载任务的对象。 Worker 继承 了 AQS, 自 己实现了简单不可重入独占锁, 其中 state=O 表示锁未被获取状态, state=l 表示锁己经 被获取的状态, state=1 是创建 Worker 时默认的状态,创建时状态设置为-1 是为了避免 该线程在运行 runWorker() 方法前被 中断,下面会具体讲解。其中变量 自rstTask 记录该工 作线程执行的第一个任务, thread 是具体执行任务的线程。
    DefaultThreadFactory 是 线程工厂, newThread 方 法是对 线程 的 一 个修饰。 其中 poolNumber 是个静态的原子变量, 用来统计线程工厂的个数, threadNumber 用来记录每 个线程工厂创建了多少线程, 这两个值也作为线程池和线程的名称的一部分。

     workQueue :用于保存等待执行的任务的阻 塞 队列,比如基于数组的有界 ArrayBlockingQueue、基于链表的无界 LinkedBlockingQueue、最多只有一个元素的 同步队列 SynchronousQueue 及优先级队列 PriorityB lockingQueue 等。 

 

ThreadPoolExecutor的策略:

线程池本身有一个调度线程,这个线程就是用于管理布控整个线程池里的各种任务和事务,例如创建线程、销毁线程、任务队列管理、线程队列管理等等。

故线程池也有自己的状态。ThreadPoolExecutor类中定义了一个volatile int变量runState来表示线程池的状态 ,分别为RUNNING、SHURDOWN、STOP、TIDYING 、TERMINATED。

  • 线程池创建后处于RUNNING状态。(运行状态)

  • 调用shutdown()方法后处于SHUTDOWN状态,线程池不能接受新的任务,清除一些空闲worker,会等待阻塞队列的任务完成。(自闭状态,不与外界交流)

  • 调用shutdownNow()方法后处于STOP状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。此时,poolsize=0,阻塞队列的size也为0。(大清除状态,将内部所有线程清除并自闭)

  • 当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。接着会执行terminated()函数。(店铺转让状态,任务都执行完/都丢弃此时生意不行挂转让)

    所有任务都执行完(包含阻塞队列里面的任务)后当前线程池活动线程 数为 0, 将要调用 terminated 方法。 

    ThreadPoolExecutor中有一个控制状态的属性叫ctl,它是一个AtomicInteger类型的变量。

  • 线程池处在TIDYING状态时,执行完terminated()方法之后,就会由 TIDYING -> TERMINATED, 线程池被设置为TERMINATED状态。( 转让成功状态。 terminated 方法调用完成以后的状态。)

线程池状态转换列举如下 。
• RUNNING -> SHUTDOWN : 显式调用 shutdown() 方法, 或者隐式调用了 finalize() 方法里面的 shutdown() 方法。

• RUNNING 或 SHUTDOWN-> STOP : 显式调用 shutdownNow() 方法时。

• SHUTDOWN -> TIDYING : 当线程池和任务队列都为空时。

• STOP -> TIDYING : 当线程池为空时。

• TIDYING-> TERMINATED : 当 terminated() hook 方法执行完成时 。

volatile int runState;//用来记录线程池状态

static final int RUNNING   = 0;

static final int SHUTDOWN   = 1;

static final int STOP      = 2;

static final int TERMINATED  = 3;

三、源码分析

ThreadPoolExecutor的实现实际是一个生产-消费模型,当用户添加任务到线程池时相当于生产者生产元素,workers中的线程直接执行任务或者从任务队列里面获取任务(当没有空闲的Worker时,任务会被暂存于任务队列中)时相当于消费者消费元素。

1、处理任务的核心方法是execute,我们看看 JDK 1.8 源码中ThreadPoolExecutor是如何处理线程任务的:

// JDK 1.8 
public void execute(Runnable command) {
    if (command == null)//任务为空抛出异常
        throw new NullPointerException();   
    int c = ctl.get();//获取线程池的状态和线程数
    // 1.当前线程数小于corePoolSize,则调用addWorker创建核心线程执行任务(核心线程还没用完)
    if (workerCountOf(c) < corePoolSize) {
       if (addWorker(command, true))
           return;
       c = ctl.get();
    }
    // 2.如果不小于corePoolSize,则将任务添加到workQueue队列。(核心线程用完则将任务添加到阻塞队列等待空闲的线程执行)
    if (isRunning(c) && workQueue.offer(command)) {//进来了证明状态是run且插入队列成功
        int recheck = ctl.get();//二次检查(有可能线程池的状态己经变化了。这里进 行二次校验)
        // 2.1 如果isRunning返回false(状态检查,不为运行状态则先删除这个任务再执行拒绝策略),则remove这个任务,然后执行拒绝策略。
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // 2.2 线程池处于running状态,但是没有线程,则创建线程(重新判断当前线程池里面是否还有 线程,如果没有则新增一个线程。)
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 3.如果放入workQueue失败,则创建非核心线程执行任务,
    // 如果这时创建非核心线程失败(当前线程总数不小于maximumPoolSize时),就会执行拒绝策略。
    else if (!addWorker(command, false))
         reject(command);
}

ctl.get()是获取线程池状态,用int类型表示。第二步中,入队前进行了一次isRunning判断,入队之后,又进行了一次isRunning判断。

为什么要二次检查线程池的状态?

在多线程的环境下,线程池的状态是时刻发生变化的。很有可能刚获取线程池状态后线程池状态就改变了。判断是否将command加入workqueue是线程池之前的状态。倘若没有二次检查,万一线程池处于非RUNNING状态(在多线程环境下很有可能发生),那么command永远不会执行。

总结一下处理流程

  1. 线程总数量 < corePoolSize,无论线程是否空闲,都会新建一个核心线程执行任务(让核心线程数量快速达到corePoolSize,在核心线程数量 < corePoolSize时)。注意,这一步需要获得全局锁。

  2. 线程总数量 >= corePoolSize时,新来的线程任务会进入任务队列中等待,然后空闲的核心线程会依次去缓存队列中取任务来执行(体现了线程复用)。

  3. 当缓存队列满了,说明这个时候任务已经多到爆棚,需要一些“临时工”来执行这些任务了。于是会创建非核心线程去执行这个任务。注意,这一步需要获得全局锁。

  4. 缓存队列满了, 且总线程数达到了maximumPoolSize,则会采取上面提到的拒绝策略进行处理。

整个过程如图所示:

我们知道线程池的特点就是线程复用和拒绝策略等,那其线程复用是怎么实现的?

ThreadPoolExecutor如何做到线程复用的?

我们知道,一个线程在创建的时候会指定一个线程任务,当执行完这个线程任务之后,线程自动销毁。但是线程池却可以复用线程,即一个线程执行完线程任务后不销毁,继续执行另外的线程任务。那么,线程池如何做到线程复用呢?

原来,ThreadPoolExecutor在创建线程时,会将线程封装成工作线程worker,并放入工作线程组(workers)中,然后这个worker反复从阻塞队列中拿任务去执行。话不多说,我们继续看看源码。

这里的addWorker方法是在上面提到的execute方法里面调用的,看看源码:

// 添加一个Worker
private boolean addWorker(Runnable firstTask, boolean core) {

    //1、 此循环用于增加Worker个数
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 当线程池状态为SHUTDOWN、STOP、TIDYING或TERMINATED时将不再增加Worker来处理任务,
        // 但要排除线程池状态刚转为SHUTDOWN且
        // ((设置了Worker过期时间且所有Worker均被回收)或(未设置Worker过期时间且Worker个数小于corePoolSize))
        // 但任务队列还有任务的情况。
        // 因为由SHUTDOWN状态的定义可知线程池会拒绝新任务但会处理任务队列里面剩余任务。
        // firstTask==null表示此次调用addWorker方法并不是要直接给新创建的Worker分配一个任务,
        // 而是要让它从任务队列中取尝试获取一个任务。
        // 在所有Worker都被回收且任务队列非空的情况下,
        // 自然要新增Worker来处理任务队列中剩余的任务;
        // 在未设置Worker过期时间且Worker数小于corePoolSize的情况下,
        // 仍需要添加一个Worker来提高处理剩余任务的效率。
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
                firstTask == null &&
                ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            // Worker数量检测
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 成功增加了Worker个数(WorkerCount记录线程池的线程个数),直接跳出外层for循环执行实际添加Worker的代码    
            if (compareAndIncrementWorkerCount(c))
                break retry;//外层循环标记retry
            c = ctl.get();
            // 状态改变则跳出内层循环,再次执行外循环进行新的状态判断
            // 否则继续在内层循环自旋直到CAS操作成功
            if (runStateOf(c) != rs)
                continue retry;
        }
    }

    // 执行到此处说明已通过CAS操作成功增减了Worker个数
    //2、 以下代码用于实际增加Worker
    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;
            // 加独占锁是为了实现workers(线程组)同步,因为可能多个线程调用了线程池的execute方法
            mainLock.lock();
            try {
                // 重新获取线程池状态,因为有可能在获取锁之前执行了shutdown操作
                int rs = runStateOf(ctl.get());
                // 如果线程池还在运行或(线程池处于SHUTDOWN状态并且firstTast为null),执行添加Worker操作
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive())
                        throw new IllegalThreadStateException();
                    // 将新创建的Worker添加到workers线程组    
                    workers.add(w);
                    int s = workers.size();
                    // 更新线程池工作线程最大数量
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // 添加成功则启动工作线程
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

(worker对象即一个工作线程,workers线程组:存放worker的hashset,线程组的线程会去阻塞队列中去拿任务tark)

代码比较长,主要分两个部分:第一部分双重循环的目的是通过 CAS 操作增加线程数: 第二部分主要是把并发安全的任务添加到 workers 里面,并且启动任务执行。

上面用worker来封装一个线程池的线程,在添加到works成功会启动work线程的start方法则底层会调用其run方法,下面则来看看该worker类的结构、启动该线程的run方法是怎么重写的:

// Worker类部分源码
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    final Thread thread;
    Runnable firstTask;

    Worker(Runnable firstTask) {
        // 调用runWorker前禁止中断(当其他线程调用了线程池的 shutdownNow 时,如果 Worker 状 态 >=0则会中断新该线程)。这里设置了线程的状态为 -1 ,所以该线程就不会被中断了 。 
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    public void run() {
            runWorker(this);
    }
    

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // 将state置为0,允许中断
    boolean completedAbruptly = true;
    try {
        // 执行传入的任务或任务队列中的任务
        // getTask用于从任务队列中获取任务,可能会被阻塞
        while (task != null || (task = getTask()) != null) {
            w.lock();
            ...
            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 {
        // Worker被回收前执行清理工作
        processWorkerExit(w, completedAbruptly);
    }
}

在构造函数中设置Worker的状态为-1是为了避免当前Worker在调用runWorker方法前被中断(当其他线程调用了shutdownNow方法,如果Worker状态>=0则会中断该线程)。runWorker中调用unlock方法时将state置为0,使Worker线程可被中断。这里在执行具体任务期间加锁,是为了避免在任务运行期间,其他线程调用了 shutdown 后正在执行的任务被中断( shutdown 只会中断当前被阻塞挂起的线程)

首先去执行创建这个worker时就有的任务,当执行完这个任务后,worker的生命周期并没有结束,在while循环中,worker会不断地调用getTask方法从阻塞队列中获取任务然后调用task.run()执行任务,从而达到复用线程的目的。只要getTask方法不返回null,此线程就不会退出。(获取到任务后并加锁,防止其他线程修改该任务)

当然,核心线程池中创建的线程想要拿到阻塞队列中的任务,先要判断线程池的状态,如果STOP或者TERMINATED,返回null

上面work中写了run中runWork方法具体的步骤,下面来分析runWork()获取等待队列的任务task方法getTask()、和Worker被回收前执行的清理工作方法processWorkerExit()。

1、getTask()方法:

// Worker.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.
        //如果线程池处于STOP、TIDYING、TERMINATED三种状态之一,或者处于SHUTDOWN状态且阻塞队列为空时,执行decrementWorkerCount()减少一个线程数(因为前面是加了1的),并返回null(接收到null则会进行回收线程)
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        // 1.allowCoreThreadTimeOut变量默认是false,核心线程即使空闲也不会被销毁
        // 如果为true,核心线程在keepAliveTime内如果空闲则会被销毁。 
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        // 2.如果运行线程数超过了最大线程数,但是缓存队列已经空了,这时递减worker数量。(回收线程) 
     // 如果有设置允许线程超时或者线程数量超过了核心线程数量,
        // 并且线程在规定时间内均未poll到任务且队列为空则递减worker数量
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // 3.1(非核心线程)如果timed为true(想想哪些情况下timed为true,就是非核心线程),则会调用workQueue的poll方法获取任务.
            // 超时时间是keepAliveTime。如果超过keepAliveTime时长,
            // poll返回了null,上边提到的while循序就会退出,线程也就执行完了。

            // 3.2(核心线程)如果timed为false(allowCoreThreadTimeOut为falsefalse
            // 且wc > corePoolSize为false)(核心线程),则会调用workQueue的take方法阻塞在当前。
            // 队列中有任务加入时,线程被唤醒,take方法返回任务,并执行。
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;   //执行完了则返回null
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

核心线程的会一直卡在workQueue.take方法,被阻塞并挂起,不会占用CPU资源,直到拿到Runnable (任务)然后返回(当然如果allowCoreThreadTimeOut设置为true,那么核心线程就会去调用poll方法,因为poll可能会返回null,所以这时候核心线程满足超时条件也会被销毁)。(该源码就是要核心/非核心线程执行完队列任务、处于非运行状态(不包含shutdowm但队列还有任务的非运行状态)等返回null回去)

非核心线程会workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ,如果超时还没有拿到,下一次循环判断compareAndDecrementWorkerCount就会返回null,Worker对象的run()方法循环体的判断为null,任务结束,然后线程被系统回收 。当然在线程被回收前还会调用processWorkerExit()方法做一些清除工作,下面对于该方法进行解析:


2、processWorkerExit()方法

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 如果runWorker方法非正常退出,则将workerCount递减
    if (completedAbruptly)
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 记录任务完成个数
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

    // 尝试设置线程池状态为TERMINATED,如果当前是SHUTDOWN状态并且任务队列为空
    // 或当前是STOP状态,当前线程池里没有活动线程
    tryTerminate();

    int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {

            // 在设置了Worker过期时间的情况下,如果任务队列为空,不必新增Worker,
            // 如果不为空,当存在Worker时不必新增Worker。
            // 在没有设置过期时间的情况下,仅当线程个数小于核心线程数时增加Worker。
            // 由此可知,在不主动关闭线程池的情况下,
            // 将会一直有Worker存在来接受任务。
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; // 将不执行addWorker操作
        }
        addWorker(null, false);
    }
}

至此,从任务提交到线程回收的整个流程分析完。

而我们下面来分析下使线程池状态变化的几个方法是怎么实现的:shutdown()、shutdownNow()、awaitTermination()

 

四、改变线程池状态的几个方法

1、调用shutdown后,线程池将不再接受新任务,但任务队列中的任务还是要执行的。

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 检查是否有关闭线程池的权限
        checkShutdownAccess();
        // 设置当前线程池状态为SHUTDOWN,如果已经是SHUTDOWN则直接返回
        advanceRunState(SHUTDOWN);
        // 中断空闲的Worker
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    // 尝试将状态转为TERMINATED
    tryTerminate();
}

private static final RuntimePermission shutdownPerm = new RuntimePermission("modifyThread");

/**
 * 检查是否设置了安全管理器,是则看当前调用shutdown命令的线程是否具有关闭线程的权限,
 * 如果有还要看调用线程是否有中断工作线程的权限,
 * 如果没有权限则抛出异常
 */
private void checkShutdownAccess() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkPermission(shutdownPerm);
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers)
                security.checkAccess(w.thread);
        } finally {
            mainLock.unlock();
        }
    }
}

// ez
private void advanceRunState(int targetState) {
    for (;;) {
        int c = ctl.get();
        if (runStateAtLeast(c, targetState) ||
            ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
            break;
    }
}

// 设置所有空闲线程的中断标志
private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            // 只中断那些还没被中断的
            // 获取w的锁成功说明w在执行runWorker方法调用getTask时被阻塞,
            // 也就是说w是空闲的,那就中断它
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            // 如果只中断一个则退出循环
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        // 判断是否满足可终止条件
        // 线程池处于RUNNING状态
        // 或处于TIDYING状态(说明有其他线程调用了tryTerminate方法且即将成功终止线程池)
        // 或线程池正处于SHUTDOWN状态且任务队列不为空时不可终止
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
            return;
        // 还有Worker的话,中断一个空闲Worker后返回   
        // 正在执行任务的Worker会在执行完任务后调用tryTerminate方法 
        if (workerCountOf(c) != 0) { 
            interruptIdleWorkers(ONLY_ONE);
            return;
        }

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 设置线程池状态为TIDYING
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // 空方法,由子类继承重写,进行线程池关闭时的清理工作
                    terminated();
                } finally {
                    // 此处无需使用CAS,因为即使CAS失败也说明线程池终止了
                    ctl.set(ctlOf(TERMINATED, 0));
                    // 激活因调用条件变量termination的await系列方法而被阻塞的所有线程
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
        // else retry on failed CAS
    }
}

2、List<Runnable> shutdownNow()

调用shutdownNow后,线程池将不会再接受新任务,并且会丢弃任务队列里面的任务且中断正在执行的任务,然后立刻返回任务队列里面的任务列表。

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        // 不是interruptIdleWorkers()
        // 中断所有在运行的Worker
        interruptWorkers();
        // 将任务队列中的任务移动到tasks中
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

// 中断所有在运行的Worker
private void interruptWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers)
            w.interruptIfStarted();
    } finally {
        mainLock.unlock();
    }
}

3、boolean awaitTermination(long timeout, TimeUnit unit)

当线程调用awaitTermination后,当前线程会被阻塞,直到线程池状态变成TERMINATIED或等待超时才返回。

public boolean awaitTermination(long timeout, TimeUnit unit)
    throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (;;) {
            // 如果线程池已经终止,则直接返回
            if (runStateAtLeast(ctl.get(), TERMINATED))
                return true;
            if (nanos <= 0)
                return false;
            // 等待相应时间,线程池成功关闭后会调用termination.signalAll()将当前线程激活
            nanos = termination.awaitNanos(nanos);
        }
    } finally {
        mainLock.unlock();
    }
}

五、Executors工具类提供的四个常用线程池(这里就不对这几个线程池进行源码解析了)

Executors类中提供的几个静态方法来创建线程池。大家到了这一步,如果看懂了前面讲的ThreadPoolExecutor构造方法中各种参数的意义,那么一看到Executors类中提供的线程池的源码就应该知道这个线程池是干嘛的。

1、 newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

CacheThreadPool运行流程如下:

  1. 提交任务进线程池。

  2. 因为corePoolSize为0的关系,不创建核心线程,线程池最大为Integer.MAX_VALUE。

  3. 尝试将任务添加到SynchronousQueue队列。

  4. 如果SynchronousQueue入列成功,等待被当前运行的线程空闲后拉取执行。如果当前没有空闲线程,那么就创建一个非核心线程,然后从SynchronousQueue拉取任务并在当前线程执行。

  5. 如果SynchronousQueue已有任务在等待,入列操作将会阻塞。

当需要执行很多短时间的任务时,CacheThreadPool的线程复用率比较高, 会显著的提高性能。而且线程60s后会回收,意味着即使没有任务进来,CacheThreadPool并不会占用很多资源。

2、 newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

核心线程数量和总线程数量相等,都是传入的参数nThreads,所以只能创建核心线程,不能创建非核心线程。因为LinkedBlockingQueue的默认大小是Integer.MAX_VALUE,故如果核心线程空闲,则交给核心线程处理;如果核心线程不空闲,则入列等待,直到核心线程空闲。

与CachedThreadPool的区别

  • 因为 corePoolSize == maximumPoolSize ,所以FixedThreadPool只会创建核心线程。 而CachedThreadPool因为corePoolSize=0,所以只会创建非核心线程。

  • 在 getTask() 方法,如果队列里没有任务可取,线程会一直阻塞在 LinkedBlockingQueue.take() ,线程不会被回收。 CachedThreadPool会在60s后收回。

  • 由于线程不会被回收,会一直卡在阻塞,所以没有任务的情况下, FixedThreadPool占用资源更多

  • 都几乎不会触发拒绝策略,但是原理不同。FixedThreadPool是因为阻塞队列可以很大(最大为Integer最大值),故几乎不会触发拒绝策略;CachedThreadPool是因为线程池很大(最大为Integer最大值),几乎不会导致线程数量大于最大线程数,故几乎不会触发拒绝策略。

3、 newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

有且仅有一个核心线程( corePoolSize == maximumPoolSize=1),使用了LinkedBlockingQueue(容量很大),所以,不会创建非核心线程。所有任务按照先来先执行的顺序执行。如果这个唯一的线程不空闲,那么新来的任务会存储在任务队列里等待执行。

4、 newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

//ScheduledThreadPoolExecutor():
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

四种常见的线程池基本够我们使用了,但是《阿里把把开发手册》不建议我们直接使用Executors类中的线程池,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学需要更加明确线程池的运行规则,规避资源耗尽的风险。

但如果你及团队本身对线程池非常熟悉,又确定业务规模不会大到资源耗尽的程度(比如线程数量或任务队列长度可能达到Integer.MAX_VALUE)时,其实是可以使用JDK提供的这几个接口的,它能让我们的代码具有更强的可读性。

        线程池巧妙地使用一个 Integer 类型的原子变量来记录线程池状态和线程池中 的钱程 个数。通过线程池状态来控制任务的执行,每个 Worker 线程可以处理多个任务。线程池 通过线程的复用减少了线程创建和销毁的开销。

从上面的几个线程池设计可以看出,可以根据自己的需要设置ThreadExecutor构造方法的参数而定义自己的线程池。

int corePoolSize(设置核心线程数量), int maximumPoolSize(设置最大线程数), BlockingQueue<Runnable> workQueue(设置不同的阻塞队列),keeyAliveTime(设置存活时间), TimeUnit (设置存活时间单位), ThreadFactory (设置创建线程的工厂),RejectedExecutionHandler(设置拒绝策略/饱和策略)

上面对于线程池原理进行了较全面的分析,而想要比较熟练掌握线程池还是要自己动手写一个线程池,后面会写一个自定义的线程池。

并且后面会选择ScheduledThreadPool线程池进行分析(用了 DelayedWorkQueue,和 DelayedQueue 类似, 是一个延迟队列。
并发系列——JUC高级篇(八)线程池的实现之ScheduledThreadPoolExecutor

本文参考https://redspider.gitbook.io/concurrent/di-san-pian-jdk-gong-ju-pian/12 及《并发编程之美》一书

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值