专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

七、聊聊并发 - 深入理解线程池的实现原理

1. 线程池介绍

在客户端-服务器的网络模式下,服务端为了提高系统的请求响应速度,往往会通过多线程来提高系统的响应速度,如果每次请求都新创建一个线程的话,会给系统带来一个很大的问题,如果并发数量很多,但是每个请求的处理时间很短,这样一来会频繁的创建和销毁线程,可能出现创建和销毁线程的时间比线程处理任务的时间还要长,这样会导致系统性能瓶颈在线程的创建和销毁的时间花费上。那有没有什么办法可以让线程重复利用呢?当然是有的,这也就是我们所说的:池化。

一般情况下,我们创建出来的线程在完成任务以后会直接被JVM自动给销毁,那池化就是将线程存放到某个池子中,当某个线程完成任务以后,线程不会被销毁,而是重新又回到中,这样线程就可以被重复的被利用了,从而减少了线程频繁创建和销毁带来的时间消耗。不仅仅是线程池,我们常用的数据库连接池也是同样的道理。那使用池化的好处是什么呢

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池内部维护了线程的创建和销毁,以及内部状态的维护,我们不需要去关心这些,我们只需要根据我们的需求设置一些必要的参数执行任务就可以,但是对于线程池内部

我们,下面我们来通过ThreadPoolExecutor来一起深入一下线程池的内部实现原理。

2. ThreadPoolExecutor核心实现

ThreadPoolExecutor继承自AbstractExecutorService,实现了ExecutorService的接口。

67_1.png

ThreadPoolExecutor中有两个执行任务的方法,一个是execut()另外一个是submit(),两者的区别就是execut()提交的任务是没有返回值的,而submit方法是可以有返回值的。而且**execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。**其实点进去源码会发现submit()内部还是调用了execut()方法。所以我们这里最主要的还是研究execut()方法。

2.1 重要字段

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; //这里类似hashMap的那个capacity

// runState is stored in the high-order bits
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; // Packing and unpacking ctl 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是线程池中非常重要的一个字段,它控制着线程池中线程的运行状态和数量。它包含了两部分信息:线程运行状态(runState) 和 线程池内有效线程的数量(workerCount)。ctl使用的是一个Integer类型来保存,使用高3位来表示线程的运行状态,把上面的状态转成二进制如下:

~CAPACITY   : 11100000000000000000000000000000
CAPACITY    : 00011111111111111111111111111111
RUNNING     : 11100000000000000000000000000000
SHUTDOWN    : 00000000000000000000000000000000
STOP        : 00100000000000000000000000000000
TIDYING : 01000000000000000000000000000000 TERMINATED : 01100000000000000000000000000000 

低29位表示的是有效的线程运行数量,CAPACITY就是1左移29位减1(29个1然后 – 1得到),这个常量表示workerCount的上限值,大约是5亿。那么我们就举个例子看一下ctl值。

ctlOf       : 11100000000000000000000000000010   //运行状态 , 有2个线程的ctl值
~CAPACITY   : 11100000000000000000000000000000
CAPACITY    : 00011111111111111111111111111111
&(与)
-----------------------------------------------------------
得到runstate 和 workerCount值。 

说清楚了这几个状态值,那我们看一下这几个状态的含义:

1、 RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务;
2、 SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用shutdown()方法进入该状态);
3、 STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态;
4、 TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
5、 TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。

2.2 构造方法

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

我们来一个一个的介绍这几个参数。

1、 corePoolSize

核心线程的数量,即最小的线程数量,即使这些线程处理空闲状态,它们也不会 被销毁,除非设置了allowCoreThreadTimeOut(true)才会销毁这些核心线程。

2、 maximumPoolSize

线程池中存在的最大线程数量

3、 keepAliveTime & unit

线程池维护除非核心线程以外所允许的线程空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,非核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime,才会被回收。

4、 blockingQueue

这也是线程池中的核心字段,主要作用就是任务缓冲,当workerCount > corePoolSize 之后提交的任务才会被封装成Work添加到队列中。而且每个workQueue不一样,线程池的任务处理的方式也是不一样的。这里线程池其实内部构建了生产消费者模型,生产者不断的往队列里添加任务,消费者不断的从队列里处理任务。

下面是我们比较常用的一些队列:

67_2.png 阻塞队列.png

1、 threadFactory

用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM\_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。
 //其中的一个构造方法    
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
 long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); } static class DefaultThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) //设置非守护线程 t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) //设置线程的级别 t.setPriority(Thread.NORM_PRIORITY); return t; } } 

一般情况下根据我们的需要,我们也可以自定义一个ThreadFactory,可以参dubbo中的NamedThreadFactory。

1、 handler

当workerCount > corePoolSize 且阻塞队列blockQueue已经满了,workerCount >= maxPoolsize 会触发这个handler,满足了以上的条件,那线程池就会拒绝再接收任务,拒绝策略如下:我觉得还是直接看代码比较好
1.用调用者所在的线程来执行任务;
    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        public CallerRunsPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
 r.run(); } } } 2. 直接抛出异常,这是默认策略; public static class AbortPolicy implements RejectedExecutionHandler { public AbortPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { //抛出RejectedExecutionException异常 throw new RejectedExecutionException("Task " + r.toString() + "rejected from" + e.toString()); } } 3. 直接丢弃任务; public static class DiscardPolicy implements RejectedExecutionHandler { public DiscardPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { } } 4. 丢弃阻塞队列中靠最前的任务,并执行当前任务 public static class DiscardOldestPolicy implements RejectedExecutionHandler { public DiscardOldestPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } } } 

通过代码也可以清楚的看到每个策略的执行,线程池默认的拒绝策略是AbortPolicy直接抛出异常,但是不会中断任务。

2.3 源码分析

2.3.1 execute方法

其实线程池提供了两个方法来执行任务,一个是submit()另外一个是execute()方法。而两者的区别分别是:

1、 submit有返回值,而execute没有。
2、 execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。
3、 execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
4、 execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法

其实通过源码我们可以发现其实submit最后还是调用到了execute方法,只不过是对execute方法进行了一层包装。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
   //上面我们已经说了ctl ,记录着runState和workerCount
    int c = ctl.get();
 //判断活跃的线程是否小于核心线程数 if (workerCountOf(c) < corePoolSize) { /** * addWorker中的第二个参数表示限制添加线程的数量是根据corePoolSize来判断还是maximumPoolSize来判断; * 如果为true,根据corePoolSize来判断; * 如果为false,则根据maximumPoolSize来判断 */ if (addWorker(command, true)) return; // 重新获取ctl值 c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { //再次检查状态 int recheck = ctl.get(); //再次判断线程池状态,如果不是运行状态,由于之前已经把command添加到workQueue中了 //需要移除已经添加到队列中的任务。 if (! isRunning(recheck) && remove(command)) reject(command); /* * 获取线程池中的有效线程数,如果数量是0,则执行addWorker方法,我们下面再详细说addWorker() */ else if (workerCountOf(recheck) == 0) addWorker(null, false); } /* * 如果执行到这里,有两种情况: * 1. 线程池已经不是RUNNING状态; * 2. 线程池是RUNNING状态,但workerCount >= corePoolSize并且workQueue已满。 * 这时,再次调用addWorker方法,但第二个参数传入为false,将线程池最大线程数量的上限设置maximumPoolSize; * 如果失败则拒绝该任务 */ else if (!addWorker(command, false)) reject(command); } 

其实execute方法乍一看的话特别简单,三个主要的 if判断,分别对workerCount 、corePoolSize 、workQueue进行判断,是否满足条件。这里使用的是offer方法,不同与add()方法,当超出队列界限的时候,add()方法是抛出异常,而offer()方法是直接返回false。最主要的方法还是addWorker(),下图就是execute()方法的主要流程。

67_3.png 41.png

2.3.2 addWorker方法

当满足相应的条件检查之后,会将任务通过addWorker添加到线程池中。


private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { //获取运行状态和获线程的数量。 int c = ctl.get(); //得到线程池的运行状态 int rs = runStateOf(c); /** 这个进行了四个判断, 1.如果rs >= SHUTDOWN,则表示线程的状态不是RUNNING状态,可能是( STOP, TIDYING, 或 TERMINATED)中的一个 就不再接收新的任务。这里就是不添加Worker了,但是线程池中已存再的任务还是要继续执行的。这个是根据状态来判断 2.剩下的三个状态判断,只要有一个是false,就返回false。 如果线程池处于 SHUTDOWN,但是 firstTask==null 且 blockQueue 非空,可以允许创建Worker来处理任务。 这是因为 SHUTDOWN 的语义:不允许提交新的任务,但是要把已经进入到 workQueue 的任务执行完, 所以在满足条件的基础上,是允许创建新的 Worker 的 */ 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(); // 正常如果是 CAS 失败的话,进到下一个里层的for循环就可以了 // 可是如果是因为其他线程的操作,导致线程池的状态发生了变更,如有其他线程关闭了这个线程池 // 那么需要回到外层的for循环 if (runStateOf(c) != rs) continue retry; } } /** 到这里了,我们就认为已经可以创建线程执行任务了,至少当前检查都通过了。 */ // worker 是否已经标志 boolean workerStarted = false; //worker添加成功是否标志。 boolean workerAdded = false; Worker w = null; try { // 把 firstTask 传给 worker 的构造方法 w = new Worker(firstTask); // 取 worker 中的线程对象,Worker的构造方法会调用 ThreadFactory 来创建一个新的线程 // 后面我们会介绍 Worker对象 final Thread t = w.thread; if (t != null) { final ReentrantLock mainLock = this.mainLock; // 这个是整个线程池的全局锁,持有这个锁才能执行操作, // 关闭一个线程池需要这个锁,某个线程持有锁的时候,线程池不会被关闭 mainLock.lock(); try { int rs = runStateOf(ctl.get()); // 小于 SHUTTDOWN 那就是 RUNNING状态 if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); //这里workers是一个HashSet,workers保存着线程池中所有的线程。 workers.add(w); int s = workers.size(); //这一步就是赋值给largestPoolSize变量 if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } //如果worker添加成功,就启动线程 if (workerAdded) { //这里其实调用的是Worker类中的run方法。 t.start(); workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); } return workerStarted; }

2.3.3 Worker

线程池中的线程,都会被封装成一个Worker类对象,ThreadPoolExecutor维护的其实就是一组Worker对象,其实我们在上面addWorker()方法中也看到了 workers 其实就是一个HashSet,维护了线程池中的线程数。

Worker类中有两个属性,一个是firstTask,用来保存传入线程池中的任务,一个是thread,是在构造Worker对象的时候利用ThreadFactory来创建的线程,是用来处理任务的线程;Worker继承AQS,使用AQS实现独占锁,并且是不可重入的,构造Worker对象的时候,会把锁资源状态设置成-1,因为新增的线程,还没有处理过任务,是不允许被中断的。

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{

 private static final long serialVersionUID = 6138294804551838833L; //真正的线程本尊 final Thread thread; //为什么叫 firstTask?因为在创建线程的时候,如果同时指定了这个任务,线程起来以后需要执行的第一个任务 //那么第一个任务就是存放在这里的(线程可不止执行这一个任务) // 当然了,也可以为 null,这样线程起来了,自己到任务队列(BlockingQueue)中取任务(getTask 方法)就行了 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; } ....为了减少篇幅,这里就省略了一些代码,主要就是重写了AQS的一些方法。 } 

在调用构造方法的时候,传入的任务使用firstTask来保存,thread使用的是getThreadFactory().newThread(this) 来创建线程,这里我们要注意 newThread()参数传入的是this,这个this指的是当前的Worker对象,因为Worker本身实现了Runnable接口。所以一个Worker对象中的Thread在启动的时候会调用Worker类中的run方法。这里需要强调一点的是真正执行任务的线程是在Worker中,给Worker的参数是需要执行的任务。

2.3.4 runWorker

当我们启动线程以后,线程调用的是runWorker这个方法,线程真正执行的任务在这里。

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    // 该线程的第一个任务(如果有的话)
    Runnable task = w.firstTask;
    //之后就将firstTask置空,
 w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { //worker 在初始化的时候,可以指定 firstTask,那么第一个任务也就可以不需要从队列中获取。 //getTask方法就是从blockQueue中获取任务。 //这个方法就是循环从阻塞队列中取任务,直到队列中没有了任务,才会退出循环。 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) { // 这里不允许抛出 Throwable,所以转换为 Error thrown = x; throw new Error(x); } finally { // 也是一个钩子方法,将 task 和异常作为参数,留给需要的子类实现,我们也需记住这个方法 afterExecute(task, thrown); } } finally { task = null; //完成任务数+1 w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } } 

2.3.5 getTask

此方法主要就是从blockQueue中不停地获取任务。

private Runnable getTask() {
    //标志位。
    boolean timedOut = false;

    for (;;) {
 int c = ctl.get(); int rs = runStateOf(c); /** 这里还是对线程池的运行状态进行判断,如果rs >= SHUTDOWN说明线程池的状态不是RUNNING状态,代表着不会 再处理新的任务。 在不是RUNNING状态的前提下,判断状态是否是STOP、TIDYING、TERMINATED,即使队列不为空的情况下,这三种状 态下也不会对队列中的任务进行处理。 所以我们开篇说如果状态是SHUTDOWN 也会继续处理队列中的任务。 */ if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { //将工作的线程数 -1之后之间返回null decrementWorkerCount(); return null; } //工作线程数 int wc = workerCountOf(c); /** timed 这个字段值时true还是false,取决于 我们是否设置了 allowCoreThreadTimeOut。 当工作线程数大于核心线程数 wc > corePoolSize 会销毁除非核心线程以外的其他线程。 如果当allowCoreThreadTimeOut = true,就是核心线程也会被回收。 */ boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; /** 1. wc > maximumPoolSize: 正常情况下 工作线程数wc 是不可能大于线程池最大值线程maximumPoolSize的限制。除非在运行期间我们调用 setMaximumPoolSize()方法调整了这个最大值。 当这种情况下,如果工作线程数wc > 1 或者队列中没有任务, 都会去回收并销毁超过maximumPoolSize的线程数。 2. timed && timedOut 当 wc < maximumPoolSize,这里我们就需要看 timed && timedOut 这个是false还是true。 当代码到这里的时候 timedOut = false ,只有在一定条件下 timedOut 才会被置为true。这个一定条件就是 阻塞队列中已经没有需要处理的任务了。不着急,我们往下看。 */ if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { if (compareAndDecrementWorkerCount(c)) return null; continue; } try { /* * 根据timed来判断,如果为true,则通过阻塞队列的poll方法进行超时控制, * 如果在keepAliveTime时间内没有获取到任务,则返回null; * 否则通过take方法,如果这时队列为空,则take方法会阻塞直到队列不为空。 */ Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); if (r != null) return r; // 当队列中没有任务的时候才会将 timedOut 置为 true。 timedOut = true; } catch (InterruptedException retry) { timedOut = false; } } } 

这里getTask()方法是一个无线循环,想要跳出循环的有两种方式,一个是 return runnable ,另一个是return null。我们在把目光拉回 runWorker 方法 中,runWorker方法也是一个 While 循环,跳出循环是通过这个条件while (task != null || (task = getTask()) != null) 也就是当 getTask()==null

我们再回到getTask()方法中,getTask() 返回 null 有两处地方,第一个地方是对线程池的状态判断,这个很容易理解

if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
     decrementWorkerCount();
     return null;
  }

重要的是在第二个处这个地方

 if ((wc > maximumPoolSize || (timed && timedOut))&& (wc > 1 || workQueue.isEmpty())) {
      if (compareAndDecrementWorkerCount(c))
           return null;
      continue;
  }

重点的是在timed && timedOut这个判断条件,我们可以发现timedOut = true 是有前提条件的。前提条件就是timed =true,怎么解释呢?我们看这里timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take() workQueue.poll 和 workQueue.take 的区别在于,当队列为空的时候workQueue.take()方法会阻塞线程,直到队列不为空。而 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 方法会在keepAliveTime的时间内如果没有任务,就会返回null。所以只有当 timed = true , timedOut 才可能会是true。

getTask() 返回null以后,代码又到了runWorker 方法中的这里while (task != null || (task = getTask()) != null)此时才会退出循环,然后会执行processWorkerExit方法。

2.3.6 processWorkerExit

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 如果completedAbruptly值为true,则说明线程执行时出现了异常,需要将workerCount减1;
    // 如果线程执行时没有出现异常,说明在getTask()方法中已经已经对workerCount进行了减1操作,这里就不必再减了。  
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        decrementWorkerCount();
 final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { //统计完成的任务数 completedTaskCount += w.completedTasks; // 从workers中移除,也就表示着从线程池中移除了一个工作线程 workers.remove(w); } finally { mainLock.unlock(); } // 根据线程池状态进行判断是否结束线程池 tryTerminate(); int c = ctl.get(); /* * 当线程池是RUNNING或SHUTDOWN状态时,如果worker是异常结束,那么会直接addWorker; * 如果allowCoreThreadTimeOut=true,并且等待队列有任务,至少保留一个worker; * 如果allowCoreThreadTimeOut=false,workerCount不少于corePoolSize。 */ if (runStateLessThan(c, STOP)) { if (!completedAbruptly) { int min = allowCoreThreadTimeOut ? 0 : corePoolSize; if (min == 0 && ! workQueue.isEmpty()) min = 1; if (workerCountOf(c) >= min) return; // replacement not needed } addWorker(null, false); } } 

至此,processWorkerExit执行完之后,工作线程被销毁,以上就是整个工作线程的生命周期,从execute方法开始,Worker使用ThreadFactory创建新的工作线程,runWorker通过getTask获取任务,然后执行任务,如果getTask返回null,进入processWorkerExit方法,整个线程结束。那多余的线程是如何被销毁的呢?当线程执行runWorker()方法以后,JVM会自动的销毁线程。

3. 动态线程池 & 线程池的监控

3.1 线程池的监控指标

其实在我们实际的业务中,对于如何合理的设置线程池的各个参数是一个较难的问题,因为线程池的各个参数的设置,我们是根据实际的情况来进行配置的,我们需要考虑的因素有多,例如部署服务的服务器器配置、处理的任务是IO密集型还是CPU密集型等,其实最大的一个难题还是我们没有办法预估实际生产中什么时刻会突然有很大的流量,这会导致我们设置的线程数不能满足需要,可能会下游的任务调用失败或者是任务处理失败等情况。如何解决这些问题,就需要我们可以及时的修改这些参数配置。在美团的Java线程池实现原理及其在美团业务中的实践 的这篇文章中,给出了关于线程池实践的一些很好地经验借鉴,感兴趣的同学可以好好的读一读,感觉还是收获满满的。

他们一开始想要通过其他的方案来代替线程池,但是考虑成本和需求之间的权重,还会回归到线程池本身。通过监控线程池的一些运行状况,可以很好的知道线程池的运行状况,通过预警方式在流量激增的情况下让负责人员及时作出反应,通过调整线程池的各个参数配置来满足业务需要。那我们就简单的看一下如何对线程池进行监控以及如何对线程池进行调控,我们可以通过线程池提供的一些属性来监控线程池的运行情况。线程池提供了如下方法:

67_4.png

  • getTaskCount:线程池已经执行的和未执行的任务总数;
  • getCompletedTaskCount:返回完成的大致的任务总数。根据作者的注释来看的话,他给的解释是: 因为任务和线程的状态在计算过程中可能会动态变化,返回值只是一个近似值
  • getLargestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了maximumPoolSize;
  • getMaximumPoolSize 获取线程池允许的最大线程数。
  • getPoolSize:线程池当前的线程数量;
  • getActiveCount:当前线程池中正在执行任务的线程数量。
  • getQueue:获取当前队列的大小
  • getTaskCount:已完成和未完成的任务数量

3.2 线程池的监控实现

废话不多说直接上代码吧,这里我只是简单些了一下,通过继承ThreadPoolExecutor自定义了一个线程池。

不知道我们还记得不,上面分析源码的时候,我们再runWorker方法里面看到的那两个没有实现的方法beforeExecute()和afterExecute(),在这里我们用到了通过重写这两个方法对任务执行前和执行后做一个信息的统计。

动态线程池的话,主要就是通过那几个线程池的方法,直接修改线程池的大小。这里我就没有实现,感兴趣的同学可以自己试一下。

public class ConsumerThreadPoolExecutor extends ThreadPoolExecutor {

    private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();
    private final AtomicLong numTasks = new AtomicLong();
    private final AtomicLong totalTime = new AtomicLong();
 public ConsumerThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } public ConsumerThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); } public ConsumerThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler); } public ConsumerThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); } @Override protected void beforeExecute(Thread t, Runnable r) { startTime.set(System.currentTimeMillis()); } @Override protected void afterExecute(Runnable r, Throwable t) { numTasks.incrementAndGet(); long endTime = System.currentTimeMillis(); long taskTime = endTime - startTime.get(); totalTime.addAndGet(taskTime); System.out.println( String.format(Thread.currentThread().getName() + "-pool-monitor: Duration: %d ms, PoolSize: %d, CorePoolSize: %d, Active: %d, Completed: %d, Queue: %d,Task: %d, LargestPoolSize: %d, MaximumPoolSize: %d,KeepAliveTime: %d, isShutdown: %s, isTerminated: %s", taskTime, this.getPoolSize(), this.getCorePoolSize(), this.getActiveCount(), this.getCompletedTaskCount(),this.getQueue().size(), this.getTaskCount(), this.getLargestPoolSize(), this.getMaximumPoolSize(), this.getKeepAliveTime(TimeUnit.MILLISECONDS), this.isShutdown(), this.isTerminated()) ); } @Override protected void terminated() { try { System.out.println(String.format("线程池执行总时间: %dns, 完成任务数:%d", totalTime.get(),numTasks.get())); } finally { super.terminated(); } } public static void main(String[] args) throws InterruptedException { ConsumerThreadPoolExecutor poolExecutor = new ConsumerThreadPoolExecutor( 5, 6, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10) , new AbortPolicy()); for (int i = 0; i < 15; i++) { poolExecutor.execute(new Runnable() { @Override public void run() { try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } } 

4. 总结

这篇文章主要通过源码,来分析了一下线程池的内部执行流程和一些实现的细节及各参数的含义;还有比较重要的一点就是对线程池的一些监控,通过线程池提供的一些方法和参数,可以对线程池的运行情况进行监控。

1、 线程池的五种状态,当 runStatus > SHUTDOWN以后,线程池的阻塞队列即使还有任务,那线程池也不会进行处理了。但是STOP状态线程池还会去处理队列中的任务。
2、 当我们需要回收核心线程的时候,通过setAllowCoreThreadTimeOut(true)开启。
3、 我们需要清楚线程池中corePoolSize 、maximumPoolSize、blockQueue这个几个变量大小关系所对应线程池的执行流程。
4、 线程池是可以在运行期间动态的调整初始化的参数。通过setMaximumPoolSize、setCorePoolSize。包括我们也可以动态的调整我们传入的阻塞队列的大小。

文章永久链接:https://tech.souyunku.com/37335

未经允许不得转载:搜云库技术团队 » 七、聊聊并发 - 深入理解线程池的实现原理

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们