【并发编程】线程池实战和原理

线程池实战和原理

文章目录

  • 线程池实战和原理
    • 1. 为什么要使用线程池?
    • 2. 如何使用线程池?
    • 3. 常用API
    • 4. 线程池的核心参数
      • 4.1 阻塞队列
      • 4.2 线程工厂
        • 4.2.1 自定义线程工厂
      • 4.3 拒绝策略
        • 4.3.1 自定义拒绝策略
    • 5. Executes框架提供的默认线程池(不推荐使用)
      • 5.1 CachedThreadPool
      • 5.2 SingleThreadExecutor
      • 5.3 FixedThreadPool
      • 5.4 SingleThreadScheduledExecutor
    • 6. 线程池的最佳实践!
      • 6.1 不要使用默认的线程池创建方式!
      • 6.2 根据业务选择合适的阻塞队列
      • 6.3 根据业务定义线程工厂
      • 6.4 根据业务定义拒绝策略
    • 7. 线程池源码解析
      • 7.1 线程池核心属性
        • 7.1.2 ctl标记
        • 7.1.2 线程池的状态
      • 7.2 线程池执行任务
        • 7.2.1 将任务提交到线程池
        • 7.2.2 添加工作线程
        • 7.2.3 工作线程自旋执行任务
        • 7.2.4 从阻塞队列中获取任务
        • 7.2.5 工作线程的销毁
      • 7.3 关闭线程池
        • 7.3.1 中断所有的闲置线程
        • 7.3.2 尝试终止线程池
    • 往期回顾

1. 为什么要使用线程池?

  • 通过池化技术可以有效地减少程序中的运行的线程数量。避免线程上下文的频繁切换从而影响程序性能。
  • 创建线程需要消耗性能,而使用线程池不用频繁地创建线程达到线程复用的效果,同时可以销毁闲置的线程节约计算机资源。
  • 因为有阻塞队列、线程工厂、拒绝策略的支持,相比于直接使用线程可以支持更加复杂的业务场景。

2. 如何使用线程池?

这里展示一个简单的自定义线程池的代码,包括七个参数的定义,以及任务的提交,以及线程池的关闭。

package com.ducx.playground.client;

import java.util.concurrent.*;

/**
 * 线程池测试类
 */
public class ExecutorClient {
    public static void main(String[] args) {

        //核心线程数
        int corePoolSize = 4;

        //最大线程数
        int maxPoolSize = 8;

        //闲置线程最大存活时间
        long keepAliveTime = 10;

        //存活时间单位
        TimeUnit unit = TimeUnit.SECONDS;

        //阻塞队列
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();

        //线程工厂
        ThreadFactory threadFactory = Executors.defaultThreadFactory();

        //拒绝策略
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

        //实例化线程池
        ThreadPoolExecutor executor =
                new ThreadPoolExecutor(
                        corePoolSize,
                        maxPoolSize,
                        keepAliveTime,
                        unit,
                        workQueue,
                        threadFactory,
                        handler);

        //向线程池中提交任务
        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("Executing task!");
            }
        });

        //关闭线程池
        executor.shutdown();
    }
}

3. 常用API

方法 作用
execute(Runnable command) 执行给定的任务
shutdown() 关闭线程池
allowCoreThreadTimeOut() 开启核心线程超时
getQueue() 获取阻塞队列
getActiveCount() 获取活跃线程数
getCorePoolSize() 获取核心线程数
getMaximumPoolSize() 获取最大线程数
getKeepAliveTime() 获取线程最大闲置时间
getLargestPoolSize() 获取历史最大线程数
getPoolSize() 获取当前线程数
getRejectedExecutionHandler() 获取拒绝策略
getThreadFactory() 获取线程工厂
purge() 清除阻塞队列中所有取消的任务
shutdownNow() 中断所有线程,清空阻塞队列
getActiveCount() 获取活跃线程数
isShutdown() 返回线程池是否是Shutdown状态
isTerminated() 返回线程池是否是Terminated状态

4. 线程池的核心参数

  • 核心线程数:线程池的核心线程数。
  • 最大线程数:线程池可以创建的最大的线程数。
  • 线程最长闲置时间:超过核心线程数部分的线程当没有任务执行而闲置时的最长存活时间。
  • 闲置时间单位:上面时间的单位,可以是秒或者是毫秒等等。
  • 阻塞队列:线程池的工作队列,线程池来不及执行的任务会先保存在阻塞队列里面。
  • 线程工厂:线程池创建线程使用的工厂。
  • 拒绝策略:线程池拒绝任务之后执行的策略。

4.1 阻塞队列

阻塞队列提供了阻塞地获取或添加元素的方法。阻塞队列相对于一般的队列提供了两个比较重要的方法:

  • offer方法:当队列满了之后当有线程向队列中添加元素的时候会被阻塞,直到队列中有元素出队。
  • poll方法:当队列中没有元素的时候有线程从队列中获取元素会被阻塞,直到有元素被放到队列里面。

4.2 线程工厂

线程池通过线程工厂来创建线程,使用者可以根据具体的业务来自定义线程工厂让线程池在创建线程的时候按照我们自己的逻辑来创建线程。

我们在自定义线程工厂的时候需要实现ThreadFactory接口,在这个接口中定义了一个方法newThread(Runnable r),我们要做的事情就是实现这个方法然后再将我们自定义的工厂传到线程池的构造方法里面就可以使用了。

【并发编程】线程池实战和原理_第1张图片

4.2.1 自定义线程工厂

我们可以通过线程工厂来指定线程的线程名、线程的优先级、线程组、是否是守护线程等等。实例代码如下:

/**
 * 自定义的线程工厂
 */
public class MyThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        //设置线程名
        thread.setName("logger-thread");
        //设置优先级
        thread.setPriority(5);
        //设置为守护线程
        thread.setDaemon(true);
        return thread;
    }
}

4.3 拒绝策略

拒绝策略是在线程池中任务满了(阻塞队列、核心线程数已满)之后线程池解决任务的时候执行的策略。ThreadPoolExecutor类中提供了四种拒绝策略,其中AbortPolicy策略是默认的拒绝策略。

  • AbortPolicy:丢弃任务并且抛出RejectedExecutionException异常。
  • CallerRunsPolicy:将当前任务交给当前线程的父线程来执行。
  • DiscardOldestPolicy:丢弃最早接受的并且没有执行的任务,并且将当前任务重新提交到线程池。
  • DiscardPolicy:丢弃掉任务并且不抛出任何异常。

4.3.1 自定义拒绝策略

也许线程池提供的拒绝策略并不能满足我们的业务需求那么就需要我们自定义一个拒绝策略。自定义拒绝策略需要实现RejectedExecutionHandler接口,并实现其中的rejectedExecution方法。RejectedExecutionHandler接口和ThreadFactory接口同样很简单都是只定义了一个方法。

【并发编程】线程池实战和原理_第2张图片

我们只需要在我们自定义的拒绝策略里面添加我们需要的逻辑即可。比如在下面的代码中将被拒绝的任务序列化之后放到消息队列中。当然具体的拒绝逻辑要根据实际的业务场景来定。

/**
 *
 * @author Chenxi Du
 * 日志线程线程池的拒绝策略:将被抛弃的任务重新放到mq
 *
 */
@Slf4j
@Component
public class MoveToMqPolicy implements RejectedExecutionHandler {
    @Autowired
    private LoggerTaskProducer loggerTaskProducer;

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        log.info("========阻塞队列已满,将日志落盘任务发送到MQ");
        loggerTaskProducer.send(((LoggerTask) r).getLogTaskDTO());
        ((EipThreadPoolExecutor) executor).getRefuseVector().add(System.currentTimeMillis());
    }
}

5. Executes框架提供的默认线程池(不推荐使用)

5.1 CachedThreadPool

这是一个核心线程数为0,最大线程数为Integer.MAX_VALUE的线程池,所有任务提交到线程池都会直接新建一个工作线程来处理。这样的好处是所有任务在提交之后几乎都能立即被执行,但是如果任务量过多的话就会开启过多的线程,这会导致线程上下文频繁地切换十分影响性能。

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

5.2 SingleThreadExecutor

这是一个核心线程数为1,最大线程数也为1的线程池,并且其阻塞队列使用的是无界的队列LinkedBlockingQueue。

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

5.3 FixedThreadPool

这是一个固定大小的线程池,核心线程数和最大线程数都是构造方法中传入的线程数。

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

5.4 SingleThreadScheduledExecutor

这是一个可以执行周期性任务的线程池,因为其使用了DelayedWorkQueue这个队列。

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

6. 线程池的最佳实践!

下面四点是我们在生产环境中使用线程池的时候必须要遵守的四个原则!

6.1 不要使用默认的线程池创建方式!

上面我们提到的Executes框架为我们提供了几种可以创建默认的线程池的方式,但是在实际的生产环境中不允许使用这些默认的线程池。因为虽然这些默认的创建方法看起来比较简单,不需要自己定义线程池参数,但是实际情况下我们需要根据具体的业务情况来订制我们的线程池。

6.2 根据业务选择合适的阻塞队列

阻塞队列是线程池的一个很重要的参数,线程池来不及执行的任务需要放到阻塞队列中,之后工作线程再从阻塞队列中拿取任务执行。所以阻塞队列的选择也是很重要的,我们需要根据我们的实际业务情况来选择合适的线程池,比如LinkedBlockingQueue是一个无界的阻塞队列,我们在使用的时候就需要考虑到我们的业务量是不是特别大,这样的话会导致oom,这时我们可以考虑使用ArrayBlockingQueue来限制队列的大小。

6.3 根据业务定义线程工厂

自定义线程工厂也同样重要,比如当我们线上服务出问题的时候通过jstack来查看堆栈信息,如果每个线程池中的线程都根据其业务来区别命名那么在排查的时候就会比较清晰。

或者是线程池中的任务是一个自旋操作,那么我们最好要将其设置成守护线程,否则再程序关闭之后,线程池中的线程也并不会销毁而是一直在自旋,这样就会出生产事故。

而上面说到的只是我个人实际碰到的,线程工厂的用处还有很多。

6.4 根据业务定义拒绝策略

线程池为我们提供了四种拒绝策略,但是往往在实际的业务场景中这四种拒绝策略并不能满足我们的要求,也许我们在任务被拒绝之后还需要更加复杂的逻辑来保证业务的安全执行,那么这时候就要求我们自定义一个符合我们实际业务的拒绝策略。

7. 线程池源码解析

7.1 线程池核心属性

7.1.2 ctl标记

ctl是线程池的以及核心的属性,是一个原子类的int型变量。作用是:

  • 标记线程池当前的运行状态
  • 标记当前的工作线程数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

也许有小伙伴会很好奇为什么一个变量为什么能记录两个值?这也就是道格·李或者说是jdk源码的精妙的地方。通过位运算可以将一个值按照不同的计算方式将其转换成不同的值。而在源码中关于ctl的位运算操作如下:

private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

//根据ctl计算线程池的运行状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
//根据ctl计算当前的工作线程数量
private static int workerCountOf(int c)  { return c & CAPACITY; }
//根据指定的运行状态以及工作线程数量反向计算ctl的值
private static int ctlOf(int rs, int wc) { return rs | wc; }

7.1.2 线程池的状态

线程池有以下五中状态:

  • RUNNING:接收新的任务,并且执行阻塞队列中的任务
  • SHUTDOWN:不接收新的任务,但是执行阻塞队列中保存的任务
  • STOP:不接收新的任务,也不执行阻塞队列中的任务,并且向每个工作线程发起中断
  • TIDYING:一个过度状态,所有的任务都终止了,并且工作线程数量等于0,将要执行terminated()方法。
  • TERMINATED:terminated()方法执行完毕之后的状态,线程池彻底地停止。

7.2 线程池执行任务

7.2.1 将任务提交到线程池

【并发编程】线程池实战和原理_第3张图片

通过execute(Runnable task)方法可以将任务提交到线程池中执行。执行的逻辑就是:

  • 如果核心线程数没有满就添加核心线程来处理任务。
  • 如果核心线程数量满了的话就添加任务到阻塞队列。并且根据ctl判断任务是否应该被拒绝。
  • 如果上一步的任务入队失败的话就开启非核心线程来执行任务。
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);        
        //如果工作线程数等于0,那么就添加非核心线程        
        else if (workerCountOf(recheck) == 0)            
            addWorker(null, false);    
    }    
    //直接添加非核心线程,如果添加失败就执行拒绝策略    
    else if (!addWorker(command, false))        
        reject(command);
}

7.2.2 添加工作线程

先检查当前的线程池状态判断工作线程是否应该被添加,如果是的话就创建一个新的工作线程来执行给定过的任务并更新ctl的工作线程数量。

这个方法会在线程池状态是STOP或者是急切地想要关闭线程池的情况下返回false,当然也有可能是线程工厂返回null或者是启动线程的时候OOM的情况下返回false。

具体的操作流程总结:

  • 先自旋地根据当前的线程池状态以及工作线程数量获取一个增加工作线程的许可。
  • 之后将线程以及任务封装成Worker,在加锁的情况下将其添加到线程池中。
  • 开启这个线程。
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    //获取一个创建工作线程的许可
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        //线程池不需要创建工作线程的情况:线程池不是运行状态,并且
        //阻塞队列是空的,表示这任务需要被拒绝
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
        //创建工作线程
        for (;;) {
            int wc = workerCountOf(c);
            //如果工作线程数量超过了我们设置的上限那么就返回fasle
            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;
            //由于工作线程数量改变导致添加工作线程失败,继续里层的自旋
        }
    }
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            //添加worker的操作需要加锁
            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();
                    //将新建的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;
}

7.2.3 工作线程自旋执行任务

Worker类中的runWorker()方法是执行具体的任务的方法,这个流程中主要是维护了一个处理任务的循环,这个循环会一直从阻塞队列中获取任务来执行。如果在阻塞队列中获取元素的时候因为超时或者是中断而返回了null,那么这个流程就会结束,同时这个工作线程也会被回收。这个方法的处理逻辑如下:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    //第一次执行获取到任务之后将Worker中的任务清空
    w.firstTask = null;
    w.unlock(); // allow interrupts
    //标记线程的循环执行过程是否是突然结束的
    boolean completedAbruptly = true;
    try {
        //先判断Worker中的任务是否是空,如果是空的话就从阻塞队列中获取任务
        while (task != null || (task = getTask()) != null) {
            //在执行任务之前设置AQS的state,标记当前的Worker不是闲置的
            w.lock();
            //这里是要保证线程池在状态大于STOP的时候,所有的工作线程都被设置中断
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                //任务执行之前的前置hook
                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 {
                    //任务执行之前的后置hook
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                //将Worker中封装的当前线程完成任务数加一
                w.completedTasks++;
                //清除state的标记,表示当前工作线程是闲置状态
                w.unlock();
            }
        }
        //表示线程执行过程正常结束
        completedAbruptly = false;
    } finally {
        //工作线程的退出操作
        processWorkerExit(w, completedAbruptly);
    }
}

7.2.4 从阻塞队列中获取任务

这个方法不光是从阻塞队列中获取任务,而且也会根据当前的线程池状态来调整工作线程的数量,从而达到将闲置的线程回收的作用。如果这个方法返回null那么工作线程的循环流程也就结束了,之后就会进行工作线程的回收工作。getTask返回null的情况:

  • 如果当前线程的线程池已经不能接受任务并且工作队列中没有任务了,那么这个时候会将工作线程数量减一并且返回一个null。
  • 如果当前的工作线程数量大于核心线程数并且阻塞队列中的元素是空的,那么减小一个工作线程数量并且返回一个null。

如果当前的任务获取有超时限制,那么从阻塞队列中获取元素就会设置超时时间,超过keepAliveTime之后就会返回null从而线程被回收。这也是为什么可以认为keepAliveTime就是超过核心线程之后的线程最大闲置时间。

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        //如果线程池的状态是SHUTDOWN并且状态是STOP或者是阻塞队列为空,那么就将工作线程
        //数量减一,并且返回null
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        int wc = workerCountOf(c);
        //如果设置了核心线程有等待超时或者当前工作线程数大于核心线程数,
        //那么当前的任务获取是超时限制
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        //如果当前的工作线程数大于最大线程数或者当前的任务有获取超时
        if ((wc > maximumPoolSize || (timed && timedOut))
            //当前的工作线程数大于1或者是阻塞队列为空
            && (wc > 1 || workQueue.isEmpty())) {
            //工作线程数量减一
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        try {
            //获取任务,分为无限时间的阻塞获取和有限时间的超时获取。如果是有限
            //时间的超时获取的话会等待keepAliveTime设置的时间。
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

7.2.5 工作线程的销毁

工作线程的销毁不单单是销毁当前的工作线程而且可能需要从新添加工作线程。如果一个工作线程执行到这里那么这个这个工作线程就一定不能用了,因为他已经退出了任务处理的流程,所以当前的工作线程是必须要被销毁的。

销毁的逻辑很简单,先修改工作线程数量,再加锁从workers中移除当前的工作线程,之后调用tryTerminate尝试关闭线程池,最后判断销毁了一个工作线程之后是否需要再新建一个工作线程。

allowCoreThreadTimeOut这个参数可以配置核心线程在闲置一定时间之后是否允许被销毁。默认这个参数是false表示不销毁,也就是说当我们设置核心线程数是4的时候,我们的线程池在线程数满了之后闲置了一段时间工作线程数还是4。但是如果这个参数被设置了true,那么在闲置一段时间之后线程池中的工作线程数量为0。

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    //如果任务执行流程是因为异常而退出,那么工作线程数就没有被调整,所以在销毁
    //工作线程之前需要先将工作线程数量减一
    if (completedAbruptly)
        decrementWorkerCount();
    //在对线程池的Workers进行调整之前需要保证线程安全
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //统计当前的worker的完成任务数
        completedTaskCount += w.completedTasks;
        //从workers中移除当前的worker
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }
    //尝试终止当前的工作线程,之后在当工作线程为0的时候这个方法才会
    tryTerminate();
    int c = ctl.get();
    
    //判断在销毁一个工作线程之后是否需要再新建一个工作线程,是需要根据我们
    //配置的核心线程数以及allowCoreThreadTimeOut来决定
    
    //当前线程池的状态是RUNNING或者是SHUTDOWN,表示当前线程池没有停止
    if (runStateLessThan(c, STOP)) {
        //如果当前的工作线程不是因为业务异常而销毁
        if (!completedAbruptly) {
            //判断线程池应该保留的最小线程数,如果allowCoreThreadTimeOut
            //为true表示不保留闲置的核心线程, 那么最小线程数就是0,否则就是核心线程数
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            //如果不保留核心线程但是阻塞队列又不为空那么就保留一个线程
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            //如果当前的工作线程数大于等于之前算出来的保留的最小线程数那么就不管了
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        //添加工作线程
        addWorker(null, false);
    }
}

7.3 关闭线程池

关闭线程池需要调用shutdown方法,这个方法会将线程池的状态设置成SHUTDOWN,从而使其不接收新提交的任务。同时为所有的工作线程设置一个中断,最后通过tryTerminate终止所有的工作线程。这个方法并不会等所有的线程终止了再返回,也就是说调用了这个方法之后线程池并不是立马就关闭还是需要将剩余的任务处理完再结束。shutdown方法更像是一个通知所有工作线程工作结束了的方法,而有些线程收到消息了就收工了,有些线程手头上还有点事情还需要加会班。

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        //CAS地修改线程池的状态为SHUTDOWN,表示不再接受新的任务
        advanceRunState(SHUTDOWN);
        //将所有闲置的线程设置中断
        interruptIdleWorkers();
        //提供给ScheduledThreadPoolExecutor的hook
        onShutdown();
    } finally {
        mainLock.unlock();
    }
    //尝试关闭线程池
    tryTerminate();
}

7.3.1 中断所有的闲置线程

为什么要设置线程的中断呢?在上面的文章中说到了工作线程会在阻塞队列中阻塞地拿任务,而这里的设置中断操作就是告知被阻塞的线程从阻塞中返回,我们知道阻塞队列的阻塞获取方法是可以响应中断的。那么这一部分的线程收到中断最后就会退出任务循环的流程。

而那些正在执行任务的线程因为获取了自己worker的锁所以这里会tryLock来获取worker的锁,一旦任务执行完了那么就会唤醒当前的线程来对工作线程设置中断。

当对线程设置了中断之后,如果阻塞队列中还有任务的话,线程还是会从中拿取任务并执行,但是一旦阻塞队列空了,线程从队列中获取任务就会立马抛出异常返回。这就保证了线程是在队列中的所有任务都处理完毕之后再被销毁。

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            //如果当前的工作线程没有设置中断并且获取该worker的锁成功
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    //将该工作线程设置中断
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

7.3.2 尝试终止线程池

在线程池状态被设置成最终的TERMINATED方法之前,需要保证工作线程都已经被销毁并且阻塞队列中的任务都被处理完毕。terminated方法是一个hook默认是空实现,在terminated方法被执行之前线程池的状态会被设置成TIDYING,在执行之后会被设置成TERMINATED。

final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        //如果线程池状态是运行状态,或者状态在TIDYING之后,或者状态是SHUTDOWN
        //并且阻塞队列中任务不为空。表示不要执行终止操作,直接返回。
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
            return;
        //如果工作线程数量不为0,就中断一个线程,并且返回
        if (workerCountOf(c) != 0) { // Eligible to terminate
            interruptIdleWorkers(ONLY_ONE);
            return;
        }
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //在调用terminated之前,设置线程池状态为TIDYING
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    //hook方法默认是空实现
                    terminated();
                } finally {
                    //执行完之后设置线程池状态为TERMINATED
                    ctl.set(ctlOf(TERMINATED, 0));
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
        // else retry on failed CAS
    }
}

往期回顾

你了解spring事务和@Transactional吗?全网最详细的事务教程。

AOP源码解析

java并发包-通过ReentrantLock学习AQS

点关注不迷路!

你可能感兴趣的:(并发编程,并发编程,多线程,java,线程池,juc)