1. 背景与简介

在Java中异步任务的处理,我们通常会使用Executor框架,而ThreadPoolExecutor是JUC为我们提供的线程池实现。

线程池的优点在于规避线程的频繁创建,对线程资源统一管理,在任务到达时能快速响应。

本文从JUC的ThreadPoolExecutor源码出发来剖析线程池的实现原理。

要比较轻松地理解ThreadPoolExecutor源码,最好需要对AbstractQueuedSynchronizer, BlockingQueue, FutureTask等类有比较熟悉的认知

另外也需要对Executor框架本身有基本认识。

关于AbstractQueuedSynchronizer的源码解读,可以参考我的AQS解读

关于FutureTask的源码解读可以参考我的FutureTask解读

线程池的大致处理流程如下图所示,线程池内部有一个阻塞队列作为任务队列用以存储提交的任务,线程池中的工作线程作为消费者从阻塞队列中不断获取任务执行。



上图截取自《Java并发编程的艺术》

从逻辑角度来说,线程池可以划分出一个核心线程池,在新任务到达时,如果核心线程池未满,会创建新线程来运行任务,即便核心线程池有空闲线程。

如果核心线程池满了,则会将任务加入到任务队列中,最终被工作线程从队列中取出执行。

如果任务队列已满,只要线程数未达到最大线程数限制,会创建一个新线程来运行任务;否则会调用饱和策略来处理该任务。

我们也可以参考下面的线程池执行示意图。



上图截取自《Java并发编程的艺术》

1.1 线程池参数

这里介绍ThreadPoolExecutor中几个比较关键的变量/参数:

  • corePoolSize 核心线程数:如果线程池中线程数量小于corePoolSize,即便现有线程有空闲也会创建新线程来运行新任务
  • maximumPoolSize 最大线程数:如果线程池中线程数量大于corePoolSize并且任务队列满时会创建新线程来运行新任务
  • keepAliveTime 线程存活时间:若果线程池中线程数量大于corePoolSize,则多余的线程在空闲时间超过keepAliveTime后会退出
  • allowCoreThreadTimeout 核心线程超时控制标志位:用于标识是否keepAliveTime的效果同样作用在核心线程上。

需要注意的是线程池中的实际工作线程数可能会超过maximumPoolSize,因为这个参数是可以通过setMaximumPoolSize方法动态调整的。

下面介绍几种Executors工具类中提供的常见的基于ThreadPoolExecutor构造的参数配置

fixedThreadPool

corePoolSize : n

maximumPoolSize : n

keepAliveTime: 0

workerQueue: LinkedBlockingQueue (无界阻塞队列)

singleThreadExecutor

corePoolSize : 1

maximumPoolSize : 1

keepAliveTime: 0

workerQueue : LinkedBlockingQueue (无界阻塞队列)

cachedThreadPool

corePoolSize : 0

maximumPoolSize : Integer.MAX

keepAliveTime : 60s

workerQueue : SynchronousQueue (同步队列)

2. 生命周期

线程池的完整生命周期具有如下五个阶段:

  • RUNNING

    这是线程池的初始状态。此状态下线程池会接受新任务并且处理队列中等待的任务。
  • SHUTDOWN

    RUNNING状态下调用shutdown方法后进入此状态。此状态下线程池不接受新任务,但会处理队列中等待的任务。
  • STOP

    RUNNING/SHUTDOWN状态下调用shutdownNow方法后进入此状态。此状态下线程池不接受新任务,也不处理既有等待任务,并且会中断既有运行中的线程。
  • TIDYING

    SHUTDOWN/STOP状态会流转到此状态。此时所有任务都已运行完毕,工作线程数为0,任务队列都为空。从字面角度理解,此时线程池已经清干净了。
  • TERMINATED

    TIDYING状态下,线程池执行完terminated钩子方法后进入此状态,此时线程池已完全终止。

2.1 线程池中生命周期的表示

ctl是线程池中一个核心状态控制变量,它的类型为AtomicInteger。ctl实际上存储了两方面信息:线程数和线程池的状态。

ctl的低29位用于表示线程数,因此范围上界约为5亿;

而高3位用于表示5种生命周期状态,对应的值分别是(注意:在ctl中实际上下面的值左移29位放在高3位存储):

  • RUNNING -1
  • SHUTDOWN 0
  • STOP 1
  • TIDYING 2
  • TERMINATED 3

因此如果我们想要取出线程数就可以用ctl和(1<<29)-1作位与运算,而如果想取出线程池状态的话就用(1<<29)-1取反后和ctl作位与运算。



图为自制的线程池的生命周期状态流转示意图

3. 源码实现

接下来,分几部分介绍线程池。

  • 工作线程封装: 介绍线程池中的Worker类, runWorker, getTask, processWorkerExit等方法。
  • 提交任务的处理: execute, addWorker, addWorkerFailed等方法。
  • 关闭线程池: shutdown, shutdownNow, tryTerminate, interruptWorkers, interruptIdleWorkers等方法。
  • 饱和策略: 内置四种饱和策略简介。

3.1 工作线程的封装抽象

ThreadPoolExecutor的Worker类是一个非常重要的内部类,它是对工作线程的封装,很有必要花上功夫详细解读。

Worker类内部包含:

  • final Thread thread 对应的工作线程对象
  • Runnable firstTask 初始任务
  • volatile long completedTasks 用于统计完成的任务

    Worker类本身继承了AbstractQueuedSynchronizer,并实现了一个简单的互斥锁Mutex Lock。

3.1.1 runWorker

Worker类实现了Runnable接口,并将run方法的实现委托给了外部类ThreadPoolExecutor的runWorker方法。

  1. /**
  2. * 工作线程运行核心逻辑。
  3. * 简单来说做的事情就是不断从任务队列中拿取任务运行。
  4. */
  5. final void runWorker(Worker w) {
  6. Thread wt = Thread.currentThread();
  7. Runnable task = w.firstTask;
  8. // 把firstTask设置为null,从GC角度来看,这处代码很重要。
  9. w.firstTask = null;
  10. // 置互斥锁状态为0,此时可以被中断。
  11. w.unlock();
  12. // 用于标记完成任务时是否有异常。
  13. boolean completedAbruptly = true;
  14. try {
  15. // 循环:初始任务(首次)或者从阻塞阻塞队列里拿一个(后续)。
  16. while (task != null || (task = getTask()) != null) {
  17. /*
  18. * 获取互斥锁。
  19. * 在持有互斥锁时,调用线程池shutdown方法不会中断该线程。
  20. * 但是shutdownNow方法无视互斥锁,会中断所有线程。
  21. */
  22. w.lock();
  23. /*
  24. * 这里if做的事情就是判断是否需要中断当前线程。
  25. * 如果线程池至少处于STOP阶段,当前线程未中断,则中断当前线程;
  26. * 否则清除线程中断位。
  27. *
  28. * if条件中的Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP)
  29. * 做的事情说穿了就是清除中断位并确认目前线程池状态没有达到STOP阶段。
  30. */
  31. if ((runStateAtLeast(ctl.get(), STOP) ||
  32. (Thread.interrupted() &&
  33. runStateAtLeast(ctl.get(), STOP))) &&
  34. !wt.isInterrupted())
  35. wt.interrupt();
  36. try {
  37. // 调用由子类实现的前置处理钩子。
  38. beforeExecute(wt, task);
  39. Throwable thrown = null;
  40. try {
  41. // 真正的执行任务
  42. task.run();
  43. } catch (RuntimeException x) {
  44. thrown = x; throw x;
  45. } catch (Error x) {
  46. thrown = x; throw x;
  47. } catch (Throwable x) {
  48. thrown = x; throw new Error(x);
  49. } finally {
  50. // 调用由子类实现的后置处理钩子。
  51. afterExecute(task, thrown);
  52. }
  53. } finally {
  54. // 清空task, 计数器+1, 释放互斥锁。
  55. task = null;
  56. w.completedTasks++;
  57. w.unlock();
  58. }
  59. }
  60. completedAbruptly = false;
  61. } finally {
  62. /*
  63. * 处理工作线程退出。
  64. * 上面主循环中的前置处理、任务调用、后置处理都是可能会抛出异常的。
  65. */
  66. processWorkerExit(w, completedAbruptly);
  67. }
  68. }

3.1.2 getTask

  1. /**
  2. * 工作线程从任务队列中拿取任务的核心方法。
  3. * 根据配置决定采用阻塞或是时限获取。
  4. * 在以下几种情况会返回null从而接下来线程会退出(runWorker方法循环结束):
  5. * 1. 当前工作线程数超过了maximumPoolSize(由于maximumPoolSize可以动态调整,这是可能的)。
  6. * 2. 线程池状态为STOP (因为STOP状态不处理任务队列中的任务了)。
  7. * 3. 线程池状态为SHUTDOWN,任务队列为空 (因为SHUTDOWN状态仍然需要处理等待中任务)。
  8. * 4. 根据线程池参数状态以及线程是否空闲超过keepAliveTime决定是否退出当前工作线程。
  9. */
  10. private Runnable getTask() {
  11. // 上次从任务队列poll任务是否超时。
  12. boolean timedOut = false;
  13. for (;;) {
  14. int c = ctl.get();
  15. int rs = runStateOf(c);
  16. /*
  17. * 如果线程池状态已经不是RUNNING状态了,则设置ctl的工作线程数-1
  18. * if条件等价于 rs >= STOP || (rs == SHUTDOWN && workQueue.isEmpty())
  19. */
  20. if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
  21. decrementWorkerCount();
  22. return null;
  23. }
  24. int wc = workerCountOf(c);
  25. /*
  26. * allowCoreThreadTimeOut是用于设置核心线程是否受keepAliveTime影响。
  27. * 在allowCoreThreadTimeOut为true或者工作线程数>corePoolSize情况下,
  28. * 当前工作线程受keepAliveTime影响。
  29. */
  30. boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
  31. /*
  32. * 1. 工作线程数>maximumPoolSize,当前工作线程需要退出。
  33. * 2. timed && timedOut == true说明当前线程受keepAliveTime影响且上次获取任务超时。
  34. * 这种情况下只要当前线程不是最后一个工作线程或者任务队列为空,则可以退出。
  35. *
  36. * 换句话说就是,如果队列不为空,则当前线程不能是最后一个工作线程,
  37. * 否则退出了就没线程处理任务了。
  38. */
  39. if ((wc > maximumPoolSize || (timed && timedOut))
  40. && (wc > 1 || workQueue.isEmpty())) {
  41. // 设置ctl的workCount减1, CAS失败则需要重试(因为上面if中的条件可能不满足了)。
  42. if (compareAndDecrementWorkerCount(c))
  43. return null;
  44. continue;
  45. }
  46. try {
  47. // 根据timed变量的值决定是时限获取或是阻塞获取任务队列中的任务。
  48. Runnable r = timed ?
  49. workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
  50. workQueue.take();
  51. if (r != null)
  52. return r;
  53. // workQueue.take是不会返回null的,因此说明poll超时了。
  54. timedOut = true;
  55. } catch (InterruptedException retry) {
  56. // 在阻塞队列上等待时如果被中断,则清除超时标识重试一次循环。
  57. timedOut = false;
  58. }
  59. }
  60. }

3.1.3 processWorkerExit

在工作线程退出的时候,processWorkerExit方法会被调用。

  1. private void processWorkerExit(Worker w, boolean completedAbruptly) {
  2. /*
  3. * 因为正常退出,workerCount减1这件事情是在getTask拿不到任务的情况下做掉的。
  4. * 所以在有异常的情况下,需要在本方法里给workCount减1。
  5. */
  6. if (completedAbruptly)
  7. decrementWorkerCount();
  8. final ReentrantLock mainLock = this.mainLock;
  9. mainLock.lock();
  10. try {
  11. // 累加completedTaskCount,从工作线程集合移除自己。
  12. completedTaskCount += w.completedTasks;
  13. workers.remove(w);
  14. } finally {
  15. mainLock.unlock();
  16. }
  17. // 由于workCount减1,需要调用tryTerminate方法。
  18. tryTerminate();
  19. int c = ctl.get();
  20. // 只要线程池还没达到STOP状态,任务队列中的任务仍然是需要处理的。
  21. if (runStateLessThan(c, STOP)) {
  22. if (!completedAbruptly) {
  23. /*
  24. * 确定在RUNNING或SHUTDOWN状态下最少需要的工作线程数。
  25. *
  26. * 默认情况下,核心线程不受限制时影响,
  27. * 在这种情况下核心线程数量应当是稳定的。
  28. * 否则允许线程池中无线程。
  29. */
  30. int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
  31. // 如果任务队列非空,至少需要1个工作线程。
  32. if (min == 0 && ! workQueue.isEmpty())
  33. min = 1;
  34. // 无需补偿工作线程。
  35. if (workerCountOf(c) >= min)
  36. return;
  37. }
  38. // 异常退出或者需要补偿一个线程的情况下,加一个空任务工作线程。
  39. addWorker(null, false);
  40. }
  41. }

3.2 提交任务的处理

3.2.1 execute

  1. /**
  2. * execute方法可以说是线程池中最核心的方法,
  3. * 在继承链上层的AbstractExecutorService中将各种接受新任务的方法最终转发给了此方法进行任务处理。
  4. */
  5. public void execute(Runnable command) {
  6. if (command == null)
  7. throw new NullPointerException();
  8. /*
  9. * 分类讨论:
  10. * 1. 如果当前线程数<核心线程数,则会开启一个新线程来执行提交的任务。
  11. *
  12. * 2. 尝试向任务队列中添加任务。这时需要再次检查方法开始到当前时刻这段间隙,
  13. * 线程池是否已经关闭了/线程池中没有工作线程了。
  14. * 如果线程池已经关闭了,需要在任务队列中移除先前提交的任务。
  15. * 如果没有工作线程了,则需要添加一个空任务工作线程用于执行提交的任务。
  16. *
  17. * 3. 如果无法向阻塞队列中添加任务,则尝试创建一个新的线程执行任务。
  18. * 如果失败,回调饱和策略处理任务。
  19. */
  20. int c = ctl.get();
  21. // 线程数 < corePoolSize
  22. if (workerCountOf(c) < corePoolSize) {
  23. if (addWorker(command, true))
  24. return;
  25. c = ctl.get();
  26. }
  27. // 检查线程池是否处于运行状态,并向任务队列中添加任务
  28. if (isRunning(c) && workQueue.offer(command)) {
  29. int recheck = ctl.get();
  30. /*
  31. * 再次检查是否线程池处于运行状态,如果不是则移除任务并回调饱和策略拒绝任务。
  32. * 因为有可能上面if条件读到线程池处于运行状态,而后shutdown/shutdownNow方法被调用,
  33. * 这时候需要把尝试刚才加入任务队列中的任务移除。
  34. */
  35. if (! isRunning(recheck) && remove(command))
  36. reject(command);
  37. // 如果workerCount为0,需要添加一个工作线程用于执行提交的任务
  38. else if (workerCountOf(recheck) == 0)
  39. addWorker(null, false);
  40. }
  41. /*
  42. * 添加一个新的工作线程处理任务。
  43. * 如果失败,则说明线程池已经关闭或者已经饱和了,此时回调饱和策略来拒绝任务。
  44. */
  45. else if (!addWorker(command, false))
  46. reject(command);
  47. }

3.2.2 addWorker

  1. private boolean addWorker(Runnable firstTask, boolean core) {
  2. retry:
  3. for (;;) {
  4. int c = ctl.get();
  5. int rs = runStateOf(c);
  6. /*
  7. * 如果线程池状态至少为STOP,返回false,不接受任务。
  8. * 如果线程池状态为SHUTDOWN,并且firstTask不为null或者任务队列为空,同样不接受任务。
  9. */
  10. if (rs >= SHUTDOWN &&
  11. ! (rs == SHUTDOWN &&
  12. firstTask == null &&
  13. ! workQueue.isEmpty()))
  14. return false;
  15. for (;;) {
  16. int wc = workerCountOf(c);
  17. /*
  18. * CAPACITY为(1<<29)-1,这是线程池中线程数真正的上界,绝不允许超过。
  19. * 因为ThreadPoolExecutor设计中是用低29位表示工作线程数的。
  20. *
  21. * 否则根据参数中是否以corePoolSize为上界进行判断,如果超过,则新增worker失败。
  22. */
  23. if (wc >= CAPACITY ||
  24. wc >= (core ? corePoolSize : maximumPoolSize))
  25. return false;
  26. // 成功新增workCount,跳出整个循环往下走。
  27. if (compareAndIncrementWorkerCount(c))
  28. break retry;
  29. c = ctl.get();
  30. /*
  31. * 重读总控状态,如果运行状态变了,重试整个大循环。
  32. * 否则说明是workCount发生了变化,重试内层循环。
  33. */
  34. if (runStateOf(c) != rs)
  35. continue retry;
  36. }
  37. }
  38. // 运行到此处时,线程池线程数已经成功+1,下面进行实质操作。
  39. boolean workerStarted = false;
  40. boolean workerAdded = false;
  41. Worker w = null;
  42. try {
  43. w = new Worker(firstTask);
  44. final Thread t = w.thread;
  45. if (t != null) {
  46. final ReentrantLock mainLock = this.mainLock;
  47. mainLock.lock();
  48. try {
  49. // 由于获取锁之前线程池状态可能发生了变化,这里需要重新读一次状态。
  50. int rs = runStateOf(ctl.get());
  51. if (rs < SHUTDOWN ||
  52. (rs == SHUTDOWN && firstTask == null)) {
  53. if (t.isAlive()) // precheck that t is startable
  54. throw new IllegalThreadStateException();
  55. // 向工作线程集合添加新worker,更新largestPoolSize。
  56. workers.add(w);
  57. int s = workers.size();
  58. if (s > largestPoolSize)
  59. largestPoolSize = s;
  60. workerAdded = true;
  61. }
  62. } finally {
  63. mainLock.unlock();
  64. }
  65. // 成功增加worker后,启动该worker线程。
  66. if (workerAdded) {
  67. t.start();
  68. workerStarted = true;
  69. }
  70. }
  71. } finally {
  72. // worker线程如果没有成功启动,回滚worker集合和worker计数器的变化。
  73. if (! workerStarted)
  74. addWorkerFailed(w);
  75. }
  76. return workerStarted;
  77. }

3.2.3 addWorkerFailed

在新增工作线程失败的情况下,调用addWorkerFailed:

  1. 从worker集合删除失败的worker。
  2. workCount减1。
  3. 调用tryTerminate尝试终止线程池。
  1. private void addWorkerFailed(Worker w) {
  2. final ReentrantLock mainLock = this.mainLock;
  3. mainLock.lock();
  4. try {
  5. if (w != null)
  6. workers.remove(w);
  7. decrementWorkerCount();
  8. tryTerminate();
  9. } finally {
  10. mainLock.unlock();
  11. }
  12. }

3.3 线程池的关闭(shutdown与shutdownNow)

在介绍线程池关闭shutdown与shutdownNow相关源码实现时,需要先分析两个很重要的方法

  • tryTerminate

    线程池的生命周期最终态为TERMINATED,然而TERMINATED状态的演进未必是调用shutdown/shutdownNow能做到,因为TERMINATED状态下,线程池中已经没有工作线程,也没有任务队列。tryTerminate方法里面包含了一个逻辑上的责任链,将线程池状态的演进动作在线程中传播下去。

  • interruptIdleWorkers

    Worker类是线程池对工作线程的封装抽象。它主要做的事情就是不断从任务队列中取任务执行,遇到异常退出。如果在工作线程等待任务(阻塞在阻塞队列)时中断该工作线程,则工作线程会重试一次getTask的循环来获取任务,获取不到就会退出runWorker方法的大循环,从而进入processWorkerExit方法收尾。ThreadPoolExecutor中线程中断的主要方法便是interruptIdleWorkers。可以通过参数控制是否最多中断1个线程。

3.3.1 tryTerminate

这是线程池中一个很重要的方法。它是实现线程池状态从SHUTDOWN或者STOP流转到TIDYING->TERMINATED的桥梁方法。

  1. final void tryTerminate() {
  2. for (;;) {
  3. int c = ctl.get();
  4. /*
  5. * 能够进行状态流转的情况是:
  6. * 1. STOP状态
  7. * 2. SHUTDOWN并且任务队列已空。
  8. */
  9. if (isRunning(c) ||
  10. runStateAtLeast(c, TIDYING) ||
  11. (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
  12. return;
  13. /*
  14. * 这时只需要所有工作线程退出即可终止线程池。
  15. * 如果仍然有工作线程,则中断一个空闲的线程。
  16. *
  17. * 一旦空闲线程被终止,则会进入processWorkerExit方法,
  18. * 在processWorkerExit方法中即将退出的工作线程会调用tryTerminate,
  19. * 从而将终止线程池的动作通过这样的机制在线程间传播下去。
  20. */
  21. if (workerCountOf(c) != 0) {
  22. interruptIdleWorkers(ONLY_ONE);
  23. return;
  24. }
  25. final ReentrantLock mainLock = this.mainLock;
  26. mainLock.lock();
  27. try {
  28. // 这时workerCount已经为0,任务队列也已为空,状态流转到TIDYING。
  29. if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
  30. try {
  31. // 调用terminated()钩子方法。
  32. terminated();
  33. } finally {
  34. // 将线程池状态拨到TERMINATED。
  35. ctl.set(ctlOf(TERMINATED, 0));
  36. // 唤醒所有在线程池终止条件上等待的线程。
  37. termination.signalAll();
  38. }
  39. return;
  40. }
  41. } finally {
  42. mainLock.unlock();
  43. }
  44. // 线程池状态流转CAS失败的话重试循环。
  45. }
  46. }

3.3.2 interruptIdleWorkers

  1. /**
  2. * 参数中的onlyOne表示至多只中断一个工作线程。
  3. * 在tryTerminate方法读取到目前线程池离可以进入终止状态只剩下workCount降为0时,
  4. * 会调用interruptIdeleWorkers(true)。因为有可能此时其他所有线程都阻塞在任务队列上,
  5. * 只要中断任意一个线程,通过processWorkerExit -> tryTerminate ->interruptIdleWorkers,
  6. * 可以使线程中断+退出传播下去。
  7. */
  8. private void interruptIdleWorkers(boolean onlyOne) {
  9. final ReentrantLock mainLock = this.mainLock;
  10. /*
  11. * 这里加全局锁的一个很重要的目的是使这个方法串行化执行。
  12. * 否则在线程池关闭阶段,退出的线程会通过tryTerminate调用到此方法,
  13. * 并发尝试中断还未中断的线程,引发一场中断风暴。
  14. */
  15. mainLock.lock();
  16. try {
  17. for (Worker w : workers) {
  18. Thread t = w.thread;
  19. // 工作线程在处理任务阶段是被互斥锁保护的,从而这里不会中断到。
  20. if (!t.isInterrupted() && w.tryLock()) {
  21. try {
  22. t.interrupt();
  23. } catch (SecurityException ignore) {
  24. } finally {
  25. w.unlock();
  26. }
  27. }
  28. // 最多中断一个。
  29. if (onlyOne)
  30. break;
  31. }
  32. } finally {
  33. mainLock.unlock();
  34. }
  35. }

3.3.3 shutdown

shutdown方法关闭线程池是有序优雅的,线程池进入SHUTDOWN状态后不会接受新任务,但是任务队列中已有的任务会继续处理。

shutdown方法会中断所有未处理任务的空闲线程。

  1. public void shutdown() {
  2. final ReentrantLock mainLock = this.mainLock;
  3. mainLock.lock();
  4. try {
  5. checkShutdownAccess();
  6. // 状态切换到SHUTDOWN。
  7. advanceRunState(SHUTDOWN);
  8. // 中断所有空闲线程,或者说在任务队列上阻塞的线程。
  9. interruptIdleWorkers();
  10. onShutdown();
  11. } finally {
  12. mainLock.unlock();
  13. }
  14. // 尝试终止线程池(状态流转至TERMINATED)。
  15. tryTerminate();
  16. }
  17. private void interruptIdleWorkers() {
  18. interruptIdleWorkers(false);
  19. }

3.3.4 shutdownNow

shutdownNow方法关闭线程池相比shutdown就暴力了一点,会中断所有线程,哪怕线程正在执行任务。

线程池进入STOP状态后,不接受新的任务,也不会处理任务队列中已有的任务。

但需要注意的是,即便shutdownNow即便会中断正在执行任务的线程,不代表你的任务一定会挂:如果提交的任务里面的代码没有对线程中断敏感的逻辑的话,线程中断也不会发生什么。

  1. public List<Runnable> shutdownNow() {
  2. List<Runnable> tasks;
  3. final ReentrantLock mainLock = this.mainLock;
  4. mainLock.lock();
  5. try {
  6. checkShutdownAccess();
  7. // 状态切换到STOP
  8. advanceRunState(STOP);
  9. // 与SHUTDOWN不同的是,直接中断所有线程。
  10. interruptWorkers();
  11. // 将任务队列中的任务收集到tasks。
  12. tasks = drainQueue();
  13. } finally {
  14. mainLock.unlock();
  15. }
  16. // 尝试终止线程池(状态流转至TERMINATED)。
  17. tryTerminate();
  18. return tasks;
  19. }
  20. /**
  21. * 此方法只会被shutdownNow方法调用,用于中断所有工作线程。
  22. */
  23. private void interruptWorkers() {
  24. final ReentrantLock mainLock = this.mainLock;
  25. mainLock.lock();
  26. try {
  27. for (Worker w : workers)
  28. w.interruptIfStarted();
  29. } finally {
  30. mainLock.unlock();
  31. }
  32. }
  33. void interruptIfStarted() {
  34. Thread t;
  35. /*
  36. * 这里中断已经执行过初次unlock的工作线程(参考runWorker方法逻辑),
  37. * 因为如果还没有走到初次unlcok那一步的工作线程,一定会读到线程池状态至少为STOP从而退出。
  38. */
  39. if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
  40. try {
  41. t.interrupt();
  42. } catch (SecurityException ignore) {
  43. }
  44. }
  45. }
  46. /**
  47. * 将任务队列中的任务dump出来。
  48. * 这个方法执行完以后,任务队列其实可能还会有残留的任务。
  49. * 比方说:我们的任务队列用LinkedBlockingQueue,事件顺序如下:
  50. * 时刻1: 线程池状态为RUNNGING,线程A执行ThreadPoolExecutor#execute方法的
  51. * if (isRunning(c) && workQueue.offer(command))
  52. * isRunning(c)返回true,此时还未执行offer操作。
  53. * 时刻2: 线程B执行shutdownNow,切换线程池状态到STOP,接下来执行完drainQueue方法。
  54. * 时刻3: 线程A开始执行offer操作,往任务队列中添加了任务。
  55. *
  56. * 对于这种情况,确实drainQueue没有按照doc描述返回所有未执行的任务,
  57. * 但实际上在ThreadPoolExecutor#execute方法中,向任务队列中添加完任务后有个再次检查线程池状态的步骤。
  58. * 此时线程A一定能够读取到线程池状态已经不是RUNNING了,在将任务从队列中移除后会使用饱和策略处理任务。
  59. */
  60. private List<Runnable> drainQueue() {
  61. BlockingQueue<Runnable> q = workQueue;
  62. ArrayList<Runnable> taskList = new ArrayList<Runnable>();
  63. q.drainTo(taskList);
  64. if (!q.isEmpty()) {
  65. for (Runnable r : q.toArray(new Runnable[0])) {
  66. if (q.remove(r))
  67. taskList.add(r);
  68. }
  69. }
  70. return taskList;
  71. }

3.4 饱和策略

当线程池由于状态或者参数配置等原因无法执行任务时,会通过reject方法调用内置的RejectedExecutionHandler(Java并发编程实战将其译为饱和策略)处理任务。

ThreadPoolExecutor中内置四种饱和策略,并且可以通过setRejectedExecutionHandler来动态调整。

下面简单介绍一下四种饱和策略:

  • CallerRunsPolicy

    调用线程池提交任务的线程自己运行提交的任务,前提是线程池仍然处于RUNNING状态,否则任务会被静默丢弃。
  • AbortPolicy

    抛出RejectedExecutionException异常,这是线程池默认的饱和策略。
  • DiscardPolicy

    静默丢弃任务。
  • DiscardOldestPolicy

    丢弃任务队列中首部的任务,重新执行任务。

四种默认饱和策略的实现都比较简单,就不对代码作介绍了。

4. 思考与总结

线程池的大致套路读懂并不是很难,包括代码中方法、语句的作用都不难读懂。难点在于读懂整体的设计精华、每一行代码为什么这么写。

下面自问自答一些读源码过程中的思考与总结。

4.1 mainLock的作用

线程池中用于保存工作线程的是一个HashSet,还有一些统计的字段比如largestPoolSize用于统计线程池中出现过的最大线程数,completedTaskCount用于统计完成的任务数。

这些东西的更新与读取都会被mainLock保护。这里很容易有个问题,为什么不用并发容器来保存工作线程?Doug Lea在源码的doc里的描述大意是:用锁可以串行化interruptIdleWorkers方法,避免关闭线程池时大量线程并发中断其他线程。另外在shutdown/shutdownNow时由于需要遍历工作线程集合来检查权限,在检查完权限后会中断工作线程。加上锁也可以保证在检查权限与中断线程过程中,工作线程集合元素不变。

4.2 Worker为什么要实现Mutex锁

Worker类继承AQS实现了一个简单不可重入的互斥锁,在执行用户提交任务的开始时需要获取锁,任务结束时需要释放锁。锁在这里最主要的目的是为了保证被别的线程中断时处于空闲状态,即没有在执行任务。当然如果shutdownNow方法被调用时,所有的线程都会被中断不管是否处于空闲状态。

很自然会想到为什么不能复用ReentrantLock组合在里面呢?实际上这里不能用ReentrantLock,因为不能允许工作线程能够多次获取锁。

我通过翻阅Doug Lea的代码库历史,发现当时有个ThreadPoolExecutor的bug,主要的问题就在于用户提交的任务通过调用ThreadPoolExecutor#setCorePoolSize -> interruptIdleWorkers 会把任务本身对应的工作线程给中断掉,因为工作线程可以通过tryLock方法重入了锁,这是不应该出现的预期外的结果。

Doug Lea的对应修复



把Worker类改成了继承AQS,实现简单的Mutex锁。

4.3 TIDYING状态的意义

ThreadPoolExecutor很早以前是只有四种状态而没有TIDYING的。我个人对此状态存在意义的思考是,TIDYING的加入使得ThreadPoolExecutor的状态跃迁逻辑更为平滑。

Doug Lea在某次提交中加入了这个状态。

TIDYING相当于很早以前的TERMINATED,目前ThreadPoolExecutor中TIDYING和TERMINATED之间的流转在于是否完成了terminated钩子方法的调用。

5. 参考资料

  • 《Java并发编程实战》
  • 《Java并发编程的艺术》

ThreadPoolExecutor源码解读的更多相关文章

  1. 线程池ThreadPoolExecutor源码解读研究(JDK1.8)

    一.什么是线程池 为什么要使用线程池?在多线程并发开发中,线程的数量较多,且每个线程执行一定的时间后就结束了,下一个线程任务到来还需要重新创建线程,这样线程数量特别庞大的时候,频繁的创建线程和销毁线程 ...

  2. 线程池:ThreadPoolExecutor源码解读

    目录 1 带着问题去阅读 1.1 线程池的线程复用原理 1.2 线程池如何管理线程 1.3 线程池配置的重要参数 1.4 shutdown()和shutdownNow()区别 1.5 线程池中的两个锁 ...

  3. ScheduledThreadPoolExecutor源码解读

    1. 背景 在之前的博文--ThreadPoolExecutor源码解读已经对ThreadPoolExecutor的实现原理与源码进行了分析.ScheduledExecutorService也是我们在 ...

  4. 转载:ThreadPoolExecutor 源码阅读

    前言 之前研究了一下如何使用ScheduledThreadPoolExecutor动态创建定时任务(Springboot定时任务原理及如何动态创建定时任务),简单了解了ScheduledThreadP ...

  5. KClient——kafka消息中间件源码解读

    目录 kclient消息中间件 kclient-processor top.ninwoo.kclient.app.KClientApplication top.ninwoo.kclient.app.K ...

  6. Netty异步Future源码解读

    本文地址: https://juejin.im/post/5df771ee6fb9a0161d743069 说在前面 本文的 Netty源码使用的是 4.1.31.Final 版本,不同版本会有一些差 ...

  7. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  8. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  9. SDWebImage源码解读 之 UIImage+GIF

    第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...

随机推荐

  1. 深入探究JFreeChart

    1 简介 JFreeChart 是 SourceForge.net 上的一个开源项目,它的源码和 API 都可以免费获得. JFreeChart 的功能非常强大,可以实现饼图 ( 二维和三维 ) ,  ...

  2. C#各种对话框

    1.选取文件夹的FolderBrowserDialog fbd = new FolderBrowserDialog();fbd.SelectedPath = "D:\Test";i ...

  3. NPM(包管理器)作用是什么?

    NPM是随同NodeJS一起安装的包管理工具,能解决NodeJS代码部署上的很多问题,常见的使用场景有以下几种: a.允许用户从NPM服务器下载别人编写的第三方包到本地使用 b.允许用户从NPM服务器 ...

  4. wallet.metamask.io 网页版钱包 connecting unknown network导致页面卡住

    之前在还不是十分懂用的时候想要用其连接本地的打开的ganache,所以就像使用本地插件的metamask一样,点击custom rpc,然后输入http://localhost:7545,然后页面就一 ...

  5. Shell命令解析

    1.简单语法: 执行shell:                                sh executeTest.sh puttyy上跑java:                      ...

  6. MATLAB——读取xls文件内容

    [NUM,TXT,RAW]=xlsread('example',2,'B2:B261') NUM返回的是excel中的数据, TXT输出的是文本内容, RAW输出的是未处理数据 example:是需要 ...

  7. Echarts中太阳图(Sunburst)的实例

    Echarts中太阳图(Sunburst)的实例 目前在项目中要实现一个Echars中的太阳图,但是Echars中的太阳图的数据格式是一个树形结构,如下代码格式如下: var mapData = [ ...

  8. Html5 手机端网页

    <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <m ...

  9. easyui datagrid JS加载样式 表头乱

    解决方案,找了下资料,加一个遮罩层,提升用户体验. <script type="text/javascript"> var width = document.docum ...

  10. lazy-load-img.js 源码 学习笔记及原理说明

    lazy-load-img.js? 1. 什么鬼? 一个轻量级的图片懒加载,我个人很是喜欢. 2. 有什么优势? 1.原生js开发,不依赖任何框架或库 2.支持将各种宽高不一致的图片,自动剪切成默认图 ...