交流
商城
MCN
登入
注册
首页
提问
分享
讨论
建议
公告
动态
发表新帖
发表新帖
ThreadPoolExecutor 线程池解析
分享
未结
0
924
李延
LV6
2021-03-17
悬赏:20积分
# 1、ThreadPoolExecutor 功能介绍 ## 1.1 为什么要使用executor 一般来说,在开发过程中,通过创建Runnable对象,然后交由相应的Thread实例去执行它们。但在大量使用多线程时会有以下问题: - 如果需要控制Thread的状态,需要开发相应的代码,不是特别方便。 - 无法控制线程数量,如果线程过多,影响性能。 而在Executor中,我们将 线程的创建与执行进行分离。我们只需要创建一个Runnable或者Callable对象,而具体的执行。我们只需要交给Executor就行。至于合适去启动一个线程将完全交给Executor去执行。 ## 1.2 简单使用 ``` public class newExecutorTest { public static void main(String[] args) { final ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); executorService.setRejectedExecutionHandler(new RejectedTaskController()); for(int i = 0; i < 20; i ++){ executorService.execute(new Task("Task"+i)); } executorService.shutdown(); executorService.execute(new Task("Task end")); } /** * 任务类 */ private static class Task implements Runnable{ private final Date intiDate; private final String name; public Task(String name) { this.name = name; this.intiDate = new Date(); } @Override public void run() { System.out.printf("%s: Task %s Created on: %s\n", Thread.currentThread().getName(), name, intiDate); System.out.printf("%s: Task Started on: %s\n", Thread.currentThread().getName(), name ); try { Long duration = (long)(Math.random()*10); System.out.printf("%s: Task %s Doing atask during %d seconds \n", Thread.currentThread().getName(), name, duration); TimeUnit.SECONDS.sleep(duration); }catch (Exception e){ e.printStackTrace(); } System.out.printf("%s: Task Finished on: %s\n", Thread.currentThread().getName(), name ); } } public static class RejectedTaskController implements RejectedExecutionHandler{ @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.printf("RejectedTaskController: the task %s has been rejected\n",r.toString()); System.out.printf("RejectedTaskController:%s \n",executor.toString()); } } } ``` ## 1.3 构造函数 - corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去; - maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量; - keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁; - unit:keepAliveTime的单位 - workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种; - threadFactory:线程工厂,用于创建线程,一般用默认即可; - handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务; ## 1.4 使用的主要方法 ### 1.4.1 提交Runnable 与 Callable 任务 ThreadPoolExecutor 可以通过 submit 方法提交 Runnable 与 Callable 子类对象。 ### 1.4.2 批量提交任务 当我们有多个任务需要提交时,可以通过批量提交任务的方法进行提交,具体如下: ```java {@linkplain ExecutorService#invokeAll(Collection)} 提交 多个 Callable任务并 并返回所有结果 {@linkplain ExecutorService#invokeAny(Collection)} 提交 多个 Callable任务并 并返回其中一个结果 ``` ### 1.4.3 任务队列workQueue #### 1.4.4 workQueue 可以干什么 当我们提交任务时。如果当前executor正在执行的任务数量小于corePoolSize,当前任务将被直接执行。 而当正在执行的任务数量大于corePoolSize时,被提交的任务将不被立即执行,而是添加到workQueue中,等待执行。 当正在执行的某各线程完成任务后,会向workQueue中获取新的任务继续执行。 所有说,不同的workQueue决定着线程的执行先后顺序。 #### 1.4.5 有哪些workQueue 在jdk中,有一些默认的workQueue,具体有: - 直接提交队列SynchronousQueue:设置为SynchronousQueue队列,SynchronousQueue是一个特殊的BlockingQueue,它没有容量,没执行一个插入操作就会阻塞,需要再执行一个删除操作才会被唤醒,反之每一个删除操作也都要等待对应的插入操作。 - 有界的任务队列ArrayBlockingQueue:有界的任务队列可以使用ArrayBlockingQueue实现 - 无界的任务队列:有界任务队列可以使用LinkedBlockingQueue实现 - 优先任务队列:优先任务队列通过PriorityBlockingQueue实现 ### 1.4.4 拒接策略handler #### 1.4.4.1 何时拒绝 - 当executor 不处于RUNNING状态时,提交的任务将被拒绝 - 当workQueue无法添加对象,并且正在执行的任务超过maximumPoolSize时,提交的任务将被拒绝 #### 1.4.4.2 自定义拒绝策略 当executor无法再接受新的任务时(具体见1.4.4.1),我们提交的任务将会背executor拒绝。这时就需要我们指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略。 在 1.2 的示例中。我们可以看到我们自己设置了一个RejectedExecutionHandler 子类对象,当我们提交的任务被拒绝时,将会执行其方法。 同时,jdk中也有一些默认的拒绝策略,具体如下: - 1、AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作; - 2、CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行; - 3、DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交; - 4、DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失; ### 1.4.5 Executor的扩展 ThreadPoolExecutor扩展主要是围绕beforeExecute()、afterExecute()和terminated()三个接口实现的, - 1、beforeExecute:线程池中任务运行前执行 - 2、afterExecute:线程池中任务运行完毕后执行 - 3、terminated:线程池退出后执行 示例见:https://www.cnblogs.com/dafanjoy/p/9729358.html 第一章节参考:https://www.cnblogs.com/dafanjoy/p/9729358.html # 2、ThreadPoolExecutor 源码分析 ## 2.1 继承关系 ```java {@linkplain Executor} {@linkplain ExecutorService} {@linkplain AbstractExecutorService} {@linkplain ThreadPoolExecutor} ``` ### 2.1.1 Executor 主要方法: ```java {@linkplain Executor#execute(Runnable)} //向executor中提交Runnable对象 ``` Executor为接口,只定义了 Runnable任务的提交方法。 ### 2.1.1 ExecutorService 主要方法: ```java {@linkplain ExecutorService#shutdown()} 在已经提交的任务完成后关闭 {@linkplain ExecutorService#shutdownNow()} 立即关闭,并返回正在等待的任务 {@linkplain ExecutorService#isShutdown()} 判断执行器的状态是否在关闭状态 {@linkplain ExecutorService#isTerminated()} 判断执行器的状态是否在终止状态 {@linkplain ExecutorService#awaitTermination(long, TimeUnit)} 阻塞当前线程 x 时间 {@linkplain ExecutorService#submit(Runnable)} 提交 Runnable任务 {@linkplain ExecutorService#submit(Callable)} 提交 Callable任务 {@linkplain ExecutorService#submit(Runnable, Object)} 提交 Runnable任务 并返回Future {@linkplain ExecutorService#invokeAll(Collection)} 提交 多个 Callable任务并 并返回所有结果 {@linkplain ExecutorService#invokeAny(Collection)} 提交 多个 Callable任务并 并返回其中一个结果 ``` ExecutorService也是一个接口,Executor的基础上,提供对执行器的关闭方法、Callable任务的提交方法、已经批量提交任务的方法。 ### 2.1.1 AbstractExecutorService AbstractExecutorService 是一个抽象方法。其中主要实现了submit、invokeAll等 相关方法。而将核心的execute方法留给子类实现。 #### 2.1.1.1 submit 相关方法 submit一共有3个方法,具体如下: ```java /** * @throws RejectedExecutionException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public Future<!--?--> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<void> ftask = newTaskFor(task, null); execute(ftask); return ftask; } /** * @throws RejectedExecutionException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public <t> Future<t> submit(Runnable task, T result) { if (task == null) throw new NullPointerException(); RunnableFuture<t> ftask = newTaskFor(task, result); execute(ftask); return ftask; } /** * @throws RejectedExecutionException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public <t> Future<t> submit(Callable<t> task) { if (task == null) throw new NullPointerException(); RunnableFuture<t> ftask = newTaskFor(task); execute(ftask); return ftask; } ``` 我们可以看到无论是那个参数,最终都会转换为 RunnableFuture 对象,通过excure方法提交任务,而execute方法具体由子类实现。 #### 2.1.1.2 invokeAll invokeAll为批量提交任务 一共有2个方法,一个是阻塞当前线程,直到所有线程执行完成;另一个为根据参数阻塞当前线程一定时间。 代码具体如下: ```java public <t> List<future<t>> invokeAll(Collection<!--? extends Callable<T-->> tasks) throws InterruptedException { if (tasks == null) throw new NullPointerException(); ArrayList<future<t>> futures = new ArrayList<future<t>>(tasks.size()); boolean done = false; try { for (Callable<t> t : tasks) { RunnableFuture<t> f = newTaskFor(t); futures.add(f); execute(f); } for (int i = 0, size = futures.size(); i < size; i++) { Future<t> f = futures.get(i); if (!f.isDone()) { try { f.get(); } catch (CancellationException ignore) { } catch (ExecutionException ignore) { } } } done = true; return futures; } finally { if (!done) for (int i = 0, size = futures.size(); i < size; i++) futures.get(i).cancel(true); } } ``` 我们可以看到,其实对于批量提交,也是通过循环调用execute提交任务,而对于结果,也时通过循环调用Future的get方法,等待所有线程执行完任务后,返回结果。 ## 2.2 ThreadPoolExecutor 源码分析 通过对父类的分析,我们发现:目前除了核心的execute方法,其他方法基本在父类已经实现。所有接下来我们重点分析execute 方法。 ### 2.2.1 关于executor状态与当前线程数量记录 在分析execute方法前,我们需要先搞明白执行器时如何记录当前状态与正在运行的线程数的。 在ThreadPoolExectutor 中通过一个成员变量AtomicInteger ctl。来记录当前executor的运行状态和当前正在运行的线程数 两个信息。 一个int转换为二进制后会有32 位。其中 前3位用于记录executor的运行状态,剩下的几位用来记录当前运行的线程数量。 具体测试代码如下: ``` public static void main(String[] args) { //用来计数的最大位数 int COUNT_BITS = Integer.SIZE - 3; int RUNNING = -1 << COUNT_BITS; int SHUTDOWN = 0 << COUNT_BITS; int STOP = 1 << COUNT_BITS; int TIDYING = 2 << COUNT_BITS; int TERMINATED = 3 << COUNT_BITS; System.out.println("RUNNING :"+toBinaryString(RUNNING)); System.out.println("SHUTDOWN :"+toBinaryString(SHUTDOWN)); System.out.println("STOP :"+toBinaryString(STOP)); System.out.println("TIDYING :"+toBinaryString(TIDYING)); System.out.println("TERMINATED:"+toBinaryString(TERMINATED)); } ``` 结果如下: RUNNING :11100000000000000000000000000000 SHUTDOWN :00000000000000000000000000000000 STOP :00100000000000000000000000000000 TIDYING :01000000000000000000000000000000 TERMINATED:01100000000000000000000000000000 我们可以看到这5种状态分别记录于int的前3位中。 当需要判断当前是否处于某中状态时进行一些位运算即可: ``` //得到运行状态 private static int runStateOf(int c) { return c & ~CAPACITY; //c 与 CAPACITY的取反 } //得到工作线程数量 private static int workerCountOf(int c) { return c & CAPACITY; } //初始化ctl private static int ctlOf(int rs, int wc) { return rs | wc; } ``` 参考:https://blog.csdn.net/yjw123456/article/details/77719061 ### 2.2.2 execute 解析 ```java 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); } ``` 在代码中的注解如下: ```java /* * Proceed in 3 steps: * * 1. If fewer than corePoolSize threads are running, try to * start a new thread with the given command as its first * task. The call to addWorker atomically checks runState and * workerCount, and so prevents false alarms that would add * threads when it shouldn't, by returning false. * * 2. If a task can be successfully queued, then we still need * to double-check whether we should have added a thread * (because existing ones died since last checking) or that * the pool shut down since entry into this method. So we * recheck state and if necessary roll back the enqueuing if * stopped, or start a new thread if there are none. * * 3. If we cannot queue task, then we try to add a new * thread. If it fails, we know we are shut down or saturated * and so reject the task. */ ``` 在代码和注解中。我们可以看到该方法中主要分为3步。 - 1、如果当前正在运行线程少于corePoolSize,则直接通过addWorker方法,新创建一个线程执行任务。当addWorker返回true时说明创建新线程成功,则任务提交成果;如果返回false说明可能有其他线程已经创建线程,使executor容量占满,则判断其他2种情况。 - 2、当运行线程超过 corePoolSize 后,则向queue中添加任务,等待执行。此时需要重新获取状态,如果executor为停止状态,则需要对队列进行回滚操作,并且执行reject方法执行任务丢弃的方法。 - 3、最后执行尝试以maximumPoolSize 为容量上线创建新的线程。否则执行reject。 综上,所有的新线程创建都时通过addWorker方法来实现,而对于拒绝的线程通过reject方法。 ### 2.2.3 reject方法 在 2.2.2 中 我们看到当executor关闭或者无法接收新线程时,会调用reject方法。通知用户 任务添加失败。代码具体如下: ```java final void reject(Runnable command) { handler.rejectedExecution(command, this); } ``` 我们可以看到其实调用的就是 handler的方法。我们在 1.2 章节中可以看到具体的示例。 ### 2.2.4 addWorker方法 ```java 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 { // Recheck while holding lock. // Back out on ThreadFactory failure or if // shut down before lock acquired. 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; } ``` 代码虽然比较长但其实只实现了2件事情: - 判断当前正在执行的线程数小于corePoolSize 或 maximumPoolSize。其中使用CAS 防止在并发情况下判断错误。 - 创建新线程并执行。 首先代码中为一个双重循环。在这个循环中: 第一步获取当前正在运行的线程数量,并根据core参数 判断是否小于corePoolSize 或 maximumPoolSize。 如果不小于说明当前executor的线程数意见满了,无法创建新线程返回fasle。 如果小于,说明可以创建新线程。通过compareAndIncrementWorkerCount方法,变更当前正在运行的线程数量。 而此时,如果该方法返回false说明已经有其他线程变更了ctl,此时的判断就可能出错需要重新判断。 所有应该重新获取ctl值,并返回第一步重新判断。 通过上一步的判断确保现在可以创建新的线程执行任务。所有接下来主要方法为创建一个Worker对象,通过worker的成员变量获取Thread对象,并执行start方法开启新线程。 到目前为止。我们已经看到executor是如何提交一个任务,并开启一个新的线程并执行的。但我们还有以下问题在源码中没有得到解答: - 我们提交的只是Runnable对象,那么Thread 对象是何时创建的。 - 当我们提交的任务数量大于corePoolSize时会被添加到 队列中,队列中的任务又是何时执行的。 - 对于executor的beforeExecute()、afterExecute()和terminated()三个方法又时在合适调用的。 其实我们在上面可以看到我们提交的Runnable并不是直接执行的,而是将其封装到Worker中进行执行。我们上面的这些疑问都可以在worker代码中得到解答 ## 2.3 Worker代码解析 ### 2.3.1 构造函数 ```java Worker(Runnable firstTask) { setState(-1); // inhibit interrupts until runWorker this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); } ``` 在这里我们可以看到成员变量 thread 是通过executor的ThreadFactory获取到的,并且传入的Runnable对象时当前Worker,也就说我们在2.2.4的addwork方法中启动的新线程执行的是Worker的run方法。所以我们接下来看看run方法。 ### 2.3.2 run 方法 在run 方法中调用的是runWork。具体方法如下: ```java 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 pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt 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); } } ``` 在这个方法中我们看到所有的方法在一个while循环中,当循环的第一次是我们在之前传入的Runnable对象。而之后的循环中是通过getTask获取任务进行执行。在getTask中核心方法其实就是尝试在queue队列中获取任务。具体代码就不详细说明了。 所以说在executor的一个线程在执行完一个任务后,不会立即关闭当前线程。而是尝试从队列中中获取新的任务进行执行,知道队列中也没有任务为止。 之后在循环体内在执行 task.run()前和之后分别执行了beforeExecute 和 afterExecute 方法。 本章节参考文章:https://www.cnblogs.com/xinxihua/p/14461338.html # 3、代码参考 https://github.com/lioyan1994111/spring_test/blob/main/src/main/java/javaSc/thread/executor/ThreadPoolExecutorTest.java https://github.com/lioyan1994111/spring_test/blob/main/src/main/java/javaSc/thread/executor/newExecutorTest.java </t></t></t></future<t></future<t></future<t></t></t></t></t></t></t></t></t></void>
回帖
消灭零回复
提交回复
热议榜
java 相关知识分享
8
好的程序员与不好的程序员
6
写给工程师的十条精进原则
5
spring boot以jar包运行配置的logback日志文件没生成
5
一步一步分析SpringBoot启动源码(一)
5
MockMvc测试
5
【吐槽向】是不是有个吐槽的板块比较好玩
4
logstash jdbc同步mysql多表数据到elasticsearch
3
IntelliJ IDEA 优质License Server
3
.gitignore忽略规则
3
SpringBoot启动源码分析
3
一步一步分析SpringBoot启动源码(三)
3
2
一步一步分析SpringBoot启动源码(二)
2
积分不够将无法发表新帖
2
官方产品
Meta-Boot - 基于MCN
MCN - 快速构建SpringBoot应用
微信扫码关注公众号