聊聊并发:(十九)ThreadPoolExecutor线程池原理分析

前言

在之前的文章中,我们陆续对concurrent包中的主要的常用类,依次对其原理进行分析,往期文章地址:
聊聊并发:(十八)ThreadLocal分析
聊聊并发:(十七)concurrent包并发容器之Queue、BlockingQueue队列原理分析
聊聊并发:(十六)concurrent包并发容器之ConcurrentHashMap分析
聊聊并发:(十)concurrent包之ReentrantReadWriteLock分析
聊聊并发:(八)concurrent包之AbstractQueuedSynchronizer源码实现分析

本篇,作为本系列文章的收尾,我们来聊一下并发场景下最为常用的工具——线程池。

ThreadPoolExecutor概述

当我们需要使用线程的时候,可以直接通过Thread实现,但是频繁的手动去创建线程,会带来很大的资源消耗,因此,我们这时候更应该考虑使用线程池。

线程池可以为我们带来的好处:

1、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;

2、提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;

3、方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;

4、更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。

在concurrent包中, 为我们提供了工厂工具类Executors,通过它可以非常方便的创建线程池,关于Executors创建多种模式的线程池的介绍,我在之前的文章中进行过介绍,本篇就不再赘述。

这里我们来分析一下ThreadPoolExecutor的实现原理。

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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }    

ThreadPoolExecutor提供了四个构造方法,我们可以选择要传入的参数,如果不传入,则会默认指定,来分别看一下这些参数含义:

  • corePoolSize:核心线程数,即线程池中一直存活着的线程的最小数量。需要注意的是,核心线程数并不代表线程池启动后,就会立即创建这么多线程,只有当线程池接收到我们提交给他的任务后, 它才会去创建并启动一定数量的核心线程来执行这些任务。这种默认的核心线程的创建启动机制,有助于降低系统资源的消耗。

  • maximumPoolSize:线程池中允许创建的最大线程数量,当线程池的当前的核心线程全部在运行中,此时线程池接收到新的执行任务后,首先会判断线程池内部的阻塞队列 workQueue 中是否还有空间,如果有,则会将任务放入阻塞队列中,否则,会创建新的线程去进行执行;当线程数达到了最大线程数后,线程池接收到新的任务后,将会触发拒绝策略。

  • keepAliveTime:当线程池中的线程数大于核心线程数是,如果一个线程处于空闲状态,超过keepAliveTime 时间将终止该线程。这个参数的设定,需要考虑具体情况:如果要执行的任务相对较多,并且每个任务执行的时间比较短,那么可以为该参数设置一个相对较大的数值,以提高线程的利用率。

    如果执行的任务相对较少, 线程池使用率相对较低, 那么可以先将该参数设置为一个较小的数值, 通过超时停止的机制来降低系统线程资源的开销, 后续如果发现线程池的使用率逐渐增高以后, 线程池会根据当前提交的任务数自动创建新的线程。

  • unit:keepAliveTime的时间单位。

  • workQueue:线程池中存放执行任务的队列,用于保存等待执行的任务。当提交一个新的任务到线程池以后, 线程池会根据当前池子中正在运行着的线程的数量, 指定出对该任务相应的处理方式, 主要有以下几种处理方式:

    1、如果线程池中正在运行的线程数少于核心线程数,那么线程池总是倾向于创建一个新线程来执行该任务,而不是将该任务提交到该队列 workQueue 中进行等待。

    2、如果线程池中正在运行的线程数不少于核心线程数,那么线程池总是倾向于将该任务先提交到队列 workQueue 中先让其等待,而不是创建一个新线程来执行该任务。

    3、如果线程池中正在运行的线程数不少于核心线程数,并且线程池中的阻塞队列也满了使得该任务入队失败,那么线程池会去判断当前池子中运行的线程数是否已经等于了该线程池允许运行的最大线程数 maximumPoolSize。如果发现已经等于了,说明池子已满,无法再继续创建新的线程了,那么就会拒绝执行该任务。如果发现运行的线程数小于池子允许的最大线程数,那么就会创建一个线程(这里创建的线程是非核心线程)来执行该任务。

  • threadFactory:线程工厂,用于创建线程。如果我们在创建线程池的时候未指定该threadFactory参数,线程池则会使用 Executors.defaultThreadFactory()方法创建默认的线程工厂,如果我们想要为线程工厂创建的线程设置一些特殊的属性,例如:设置见名知意的名字,设置特定的优先级等等,那么我们就需要自己去实现 ThreadFactory 接口,并在实现其抽象方法 newThread()的时候,使用Thread类包含 threadName(线程名字)的那个构造方法就可以指定线程的名字(通常可以指定见名知意的名字),还可以用 setPriority() 方法为线程设置特定的优先级等。然后在创建线程池的时候,将我们自己实现的 ThreadFactory 接口的实现类对象作为 threadFactory 参数的值传递给线程池的构造方法即可。

  • handler:拒绝策略,当向线程池提交新的任务,线程池无法执行的时候,将会触发拒绝策略。当出现以下情况时,拒绝策略将会触发:

    • 当线程池处于SHUTDOWN状态时
    • 当线程池的线程数已达到最大线程数且全部为运行状态,同时阻塞队列的容量已满

    ThreadPoolExecutor支持四种拒绝策略,分别是:

    1、AbortPolicy: 直接拒绝策略,抛出 RejectedExecutionException 异常,该策略也是线程池的默认拒绝策略

    2、CallerRunsPolicy:将被拒绝的任务放在ThreadPoolExecutor.execute()方法所在的那个线程中执行。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度

    3、DiscardPolicy:将被拒绝的任务直接删除

    4、DiscardOldestPolicy:当线程池没有关闭的情况下,会将阻塞队列队首的那个任务从队列中移除,然后将被拒绝的任务加入队列的队尾

内部结构

核心参数

在分析线程池工作原理的源码之前,我们先来看一下线程池内部的一些核心参数:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

private static final int COUNT_BITS = Integer.SIZE - 3;

private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

private static int runStateOf(int c) { 
    return c & ~CAPACITY; 
}
private static int workerCountOf(int c) { 
    return c & CAPACITY; 
}
private static int ctlOf(int rs, int wc) {
    return rs | wc; 
}

ctl参数是线程池内部非常核心的一个参数,使用AtomicInteger表示,保证多线程操作下的线程安全性;

它控制着线程池的状态,一个ctl变量包含了两部分信息:线程池的运行状态和线程池内有效线程的数量。ctl是一个int类型,它用其高3位表示线程池运行状态,用低29位表示线程池内有效线程的数量。

关于这种控制方式,如果您看过ReentrantReadWriteLock的源码,一定不陌生这种方式,ReentrantReadWriteLock使用一个数值分别记录读写锁的获取次数,这种使用位操作控制的方式在concurrent包中非常常见,非常精妙。

线程池的运行状态分为以下几种状态:

  • RUNNING:运行状态,能接受新提交的任务, 并且也能处理阻塞队列中的任务
  • SHUTDOWN:关闭状态,不再接受新提交的任务, 但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时, 调用 shutdown()方法会使线程池进入到该状态。当然, finalize() 方法在执行过程中或许也会隐式地进入该状态
  • STOP:停止状态,不能接受新提交的任务,也不能处理阻塞队列中已保存的任务, 并且会中断正在处理中的任务。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态
  • TIDYING:清理状态,所有的任务都已终止了, workerCount (有效线程数) 为0, 线程池进入该状态后会调用 terminated() 方法以让该线程池进入TERMINATED 状态. 当线程池处于 SHUTDOWN 状态时, 如果此后线程池内没有线程了并且阻塞队列内也没有待执行的任务了, 线程池就会进入到该状态。当线程池处于 STOP 状态时,如果此后线程池内没有线程了, 线程池就会进入到该状态
  • TERMINATED:终止状态,当调用terminated() 方法后进入该状态

通过调用runStateOf() 方法,可以获取到当前线程池的运行状态

调用workerCountOf() 方法,可以获取到当前线程池的工作线程数目

对于我们来讲,我们可能更关注的状态是线程池是否在运行、是否准备停止、是否已经停止,
这里我们可以粗暴的预先给出结果:

runStateOf() < 0 时,表示线程池正在运行中

runStateOf() = 0 时,表示线程池准备停止

runStateOf() > 0 时,表示线程池进入停止状态(也可能是TIDYING或者TERMINATED)

Worker

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    
    private static final long serialVersionUID = 6138294804551838833L;

    final Thread thread;

    Runnable firstTask;

    volatile long completedTasks;

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

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

    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) {
            }
        }
    }
}

Worker是内部比较重要的一个对象,它是线程池内部的实际执行器,我们提交的Runnable方法,是由它进行执行的。Worker对象实现了AQS队列,也就是说它具备锁的能力,它自身实现了锁的一部分功能。

execute

介绍完构造方法与内部结构的核心参数与对象,我们接下来看一下线程池的核心实现。

execute()方法是线程池的核心方法,提交一个Runnable任务给线程池,线程池进行执行,接下来我们来看分析一下实现机制:

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);
}

上面就是execute() 方法的实现,其接收一个Runnable的参数,我们对大体流程进行梳理,在对具体细节进行分析:

1、获取ctl值

2、判断当前工作线程池数是否小于核心线程数,如果是,新增工作线程执行任务,并结束流程

3、如果不是,说明当前工作线程数已经达到核心线程数值,则判断线程池是否是运行状态,如果是,尝试将任务加入到阻塞队列中(而不是新增线程);

如果成功,再次判断线程池是否是运行状态,如果已经不再是运行状态,将刚刚新增到阻塞队列中的任务移除,并执行拒绝策略;如果依旧是运行状态,判断当前工作线程数是否为0,如果是,增加工作线程,但不启动;

4、上述条件均不满足,则尝试新增工作线程执行任务,如果失败,执行拒绝策略

我们来分步骤进行分析:

首先来看一下新增工作线程的实现,addWorker():

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            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
        }
    }

    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 {
                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);
                    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;
}

流程稍长,我们一步一步来看:

1、进入无限循环,首先获取运行状态,判断运行状态是否正常

2、循环检查当前工作线程数是否大于最大容量,或者是否大于核心线程数或最大线程数(参数控制),如果校验通过,通过CAS操作增加工作线程数,并退出循环

3、前置校验全部通过后,进入新增工作线程部分,创建Worker对象,将提交的任务放入Worker中

4、获取锁,将Worker加入工作线程组中,释放锁,启动Worker线程

addWorker() 方法的大体流程如上,这里并没有进行特别详细的分析,由于整体流程并不复杂,较为核心的部分,是Worker中执行提交任务的部分:

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 ((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);
    }
}

由于Worker本身是一个线程,当启动Worker时,会执行其run() 方法,在run() 中,调用了runWorker(),实现如上。

我们对其流程进行详细分析:

1、获取当前线程,以及Worker中的Runnable任务

2、获取锁,如果任务不为空,或getTask() 获取任务不为空时,进行while循环

3、首先会判断线程池的状态,以及任务线程的状态,是否处于被打断的状态

4、执行扩展方法beforeExecute(),在执行任务前需要进行的操作,ThreadPoolExecutor只定义了方法,没有进行实现,子类可以自行进行扩展

5、执行任务的run方法,真正的任务内容开始执行

6、执行扩展方法afterExecute(),同beforeExecute()

7、释放锁,将任务的引用置空,并进行任务执行结束后的状态处理

我们来描述一下ThreadPoolExecutor任务工作流程:

聊聊并发:(十九)ThreadPoolExecutor线程池原理分析_第1张图片

这是一个较为就简单的示意图,当我们提交一个任务到ThreadPoolExecutor线程池时,如果当前线程池没有工作线程,则会新建一个,去执行提交的任务,当工作线程数达到核心线程数时,再提交的任务会加入到阻塞队列中去;

当核心工作线程执行完毕它之前的任务时,则会从阻塞队列中获取任务,再次进行执行,而获取的操作,则是在getTask()方法中实现:

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        int wc = workerCountOf(c);
        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;
        }
    }
}

getTask() 主要流程我们看一下:

1、获取线程池运行状态

2、检查线程池的状态,如果已经是STOP及以上的状态,或者已经SHUTDOWN,队列也是空的时候,直接return null,并将Worker数量-1

3、获取从阻塞队列获取任务是否超时标志,allowCoreThreadTimeOut参数,字面意思是否允许核心线程超时,即如果我们设置为false,那么只有当线程数wc大于corePoolSize的时候才会超时,更直接的意思就是,如果设置allowCoreThreadTimeOut为false,那么线程池在达到corePoolSize个工作线程之前,不会让闲置的工作线程退出,如果设置为true,那么核心工作线程空闲后,会立刻退出

4、从队列中取任务,根据timed选择是有时间期限的等待还是无时间期限的等待,这里的等待时间是我们在构造方法中传入的keepAliveTime

看到这里,我们就可以理解在文章开头中提到的,为什么线程池可以重用已存在的线程,因为当工作线程达到核心线程数目后,如果当前线程池中没有需要执行的任务,线程池将进入空闲状态,核心工作线程将进入等待;此时当有新任务提交时,将会进入到阻塞队列中,核心线程会从阻塞队列中获取任务,进行执行,反复这个过程。

ThreadPoolExecutor工作流程

上面介绍了ThreadPoolExecutor的主要工作流程,下面我们用一张流程图对其进行梳理:

聊聊并发:(十九)ThreadPoolExecutor线程池原理分析_第2张图片

结语

本篇,我们介绍了ThreadPoolExecutor的工作原理,了解了ThreadPoolExector是如何复用线程的,以及是如何执行任务的,本篇对于线程池参数的设置没有进行过多的说明,由于之前的文章已经进行过介绍,感兴趣的读者,可以看一下之前的文章。

Java ThreadPoolExecutor线程池概述

希望本篇可以对您有所帮助。

更多Java干货文章请关注我的个人微信公众号:老宣与你聊Java

老宣与你聊Java

你可能感兴趣的:(Java多线程开发,聊聊Java并发)