Java线程池execute和submit的区别

devtools/2024/9/23 10:08:49/

前言

ThreadPoolExecutor提供了两种方法来执行异步任务,分别是execute和submit,也是日常开发中经常使用的方法,那么它俩有什么区别呢?

语义不同

首先是语义上的不同。execute声明在Executor接口,它接受一个Runnable类型的入参,且没有返回值,代表它可以异步执行一个没有返回结果的异步任务,你甚至不知道这个任务什么时候执行完毕。

java">public interface Executor {void execute(Runnable command);
}

而submit声明在ExecutorService,且有多个重载方法,最明显的区别就是submit方法均有Future返回值。Future代表未来结果的一个占位符,可以通过它拿到异步任务的执行结果,如果异步任务执行失败,也可以通过它拿到异常信息。
所以,下面三个方法的区别是:方法1的入参是Runnable没有返回结果,所以返回的Future是也拿不到结果的,只能判断异步任务是否执行完毕以及是否执行异常;方法2返回的Future可以拿到结果,值是第二个入参;方法3也可以拿到结果,值就是入参Callable返回的结果。

java">public interface ExecutorService extends Executor {[1] Future<?> submit(Runnable task);[2] <T> Future<T> submit(Runnable task, T result);[3] <T> Future<T> submit(Callable<T> task);
}

综上所述,execute和submit最明显的一个区别就是语义上的不同。execute用来执行没有返回结果的异步任务,且调用方不关心任务的执行结果;而submit用来执行有返回结果的异步任务,适用于调用方关心执行结果和异步任务执行有返回值的场景。

异常处理不同

execute和submit第二个区别是:异步任务执行异常时的处理不同。execute遇到异常会直接抛出来,而submit会默默吃掉异常。
如下示例,execute会把除0异常的堆栈打印出来,而submit则没有打印任何信息。

java">public static void main(String[] args) throws Exception {ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());executor.execute(() -> {System.err.println("execute result:");System.err.println(1 / 0);});executor.submit(() -> {System.err.println("submit result:");System.err.println(1 / 0);});}
java">execute result:
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zeroat ExecutorDemo2.lambda$main$0(ExecutorDemo2.java:18)at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)at java.base/java.lang.Thread.run(Thread.java:840)
submit result:

你可能会觉得疑惑,submit凭什么要默默吃掉异常,任务执行出错为什么不通知调用方呢?
事实上,submit针对异常的处理方式正和它的语义有关,它并没有吃掉异常,而是它认为方法执行异常也是结果的一部分,别忘了它是有Future返回值的,只不过它并没有粗暴的直接抛出异常,而是把异常记录在了Future返回值里面。如果要关心submit提交的异步任务的执行情况,可以通过下面这种方式:

java">Future<?> future = executor.submit(() -> {System.err.println("submit result:");System.err.println(1 / 0);
});
try {future.get();System.err.println("正常执行");
} catch (Exception e) {System.err.println("原来执行异常了:" + e.getMessage());
}

异常线程销毁重建

execute和submit第三个区别是:execute面对异常线程会销毁重建,而submit会继续复用异常线程。
看下面的例子,创建一个固定3个线程的线程池,先分别execute执行2个正常的任务和1个异常任务,sleep一会再次执行三个任务,我们发现3号线程因为执行异常被销毁了,线程池重新启动了4号线程。如果调用submit,则始终是一开始创建的三个线程在工作,不会出现线程的销毁和重建情况。

java">public static void main(String[] args) throws Exception {ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy());executor.execute(new Task(false));executor.execute(new Task(false));executor.execute(new Task(true));Thread.sleep(1000);System.err.println("===============");for (int i = 0; i < 3; i++) {executor.execute(new Task(false));}
}static class Task implements Runnable {private boolean throwException;public Task(boolean throwException) {this.throwException = throwException;}@SneakyThrows@Overridepublic void run() {if (throwException) {System.err.println("error:" + Thread.currentThread().getName());throw new RuntimeException();}Thread.sleep(1);System.err.println("task:" + Thread.currentThread().getName());}
}
java">error:pool-1-thread-3
task:pool-1-thread-2
task:pool-1-thread-1
Exception in thread "pool-1-thread-3" java.lang.RuntimeExceptionat ExecutorDemo$Task.run(ExecutorDemo.java:36)at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)at java.base/java.lang.Thread.run(Thread.java:840)
===============
task:pool-1-thread-1
task:pool-1-thread-4
task:pool-1-thread-2

源码浅析

最后,从源码层面看看导致execute和submit区别的原因。
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);
}

在线程池里,线程会被封装成Worker对象,它的run方法是个while循环,不停的从任务队列workQueue里面取出异步任务并执行,不过它在执行任务时,如果遇到异常,会直接抛出来。并且在finally里面会把异常线程从workers里面移除,后续当工作线程数小于核心线程数时又会继续创建新的线程。

java">final void runWorker(Worker w) {Thread wt = Thread.currentThread();Runnable task = w.firstTask;w.firstTask = null;w.unlock();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);try {task.run();afterExecute(task, null);} catch (Throwable ex) {afterExecute(task, ex);throw ex; // 直接抛出异常}} finally {task = null;w.completedTasks++;w.unlock();}}completedAbruptly = false;} finally {processWorkerExit(w, completedAbruptly);}
}

而submit之所以不会抛出异常,是因为它对提交的任务进行了一层封装,Runnable封装成了FutureTask:

java">public Future<?> submit(Runnable task) {if (task == null) throw new NullPointerException();RunnableFuture<Void> ftask = newTaskFor(task, null);execute(ftask);return ftask;
}

FutureTask就是Future的实现类,它重写了run方法,对异常进行了捕获,如果发生异常会把异常记录下来,而不是直接抛出,调用者可以通过Future来获得异常信息。

java">public void run() {if (state != NEW ||!RUNNER.compareAndSet(this, null, Thread.currentThread()))return;try {Callable<V> c = callable;if (c != null && state == NEW) {V result;boolean ran;try {result = c.call();ran = true;} catch (Throwable ex) {// 捕获异常result = null;ran = false;setException(ex);// 记录异常}if (ran)set(result);}} finally {runner = null;int s = state;if (s >= INTERRUPTING)handlePossibleCancellationInterrupt(s);}
}

尾巴

execute和submit都可以执行异步任务,但是有三大区别,分别是:1.语义上的区别,是否关心任务的执行情况和返回结果;2.遇到遇到是直接抛出还是先记录下来;3.异常线程是销毁重建还是继续复用。在源码层面,本质上submit只是针对提交的任务又做了一层包装,FutureTask重写了run()捕获了异常信息并记录了下来,方便调用者从Future里面拿到执行结果。


http://www.ppmy.cn/devtools/46588.html

相关文章

【简单学习一下卷积神经网络】-基于肆十二的高考例子

前言一、白话卷积神经网络总结 前言 【参考】 主要是P2⇨手把手教你用tensorflow2训练自己的数据集 -------2024/5/4 一、白话卷积神经网络 高考前需要大量的做题训练---->相当于数据集。 做题过程中【于标准答案进行比对】产生的错题⇨loss&#xff08;误差&#xff09; 回…

【成品设计】基于华大hc32F005c6ua的读取NFC卡

《基于华大hc32F005c6ua的读取NFC卡》 整体功能&#xff1a; 单片机:华大hc32F005c6ua 1、支持单片机spi接口读取nfc读卡器芯片rc522读写数据 2、读取到的数据可以通过单片机uart接口通信&#xff0c;上报给上位机&#xff08;485主机&#xff09; 3、uart接口支持modbus协议…

Spring Boot(七十六):集成Redisson实现布隆过滤器(Bloom Filter)

之前在redis(17):什么是布隆过滤器?如何实现布隆过滤器?中介绍了布隆过滤器,以及原理,布隆过滤器有很多实现和优化。之前我们讲解了由 Google 开发著名的 Guava 库实现布隆过滤器(Bloom Filter)。下面我们讲解基于Redisson实现布隆过滤器。 1 Redisson简介 Redisson…

解释Java中的安全模型

Java中的安全模型是一个多层次、综合性的框架&#xff0c;旨在确保Java应用程序的安全运行。开发者在设计和实现Java应用程序时&#xff0c;应该合理利用和配置Java提供的这些安全机制&#xff0c;确保应用程序的数据和用户的信息安全。以下是对Java安全模型的详细解释&#xf…

9. MySQL事务、字符集

文章目录 【 1. 事务 Transaction 】1.1 事务的基本原理1.2 MySQL 执行事务的语法和流程1.2.1 开始事务1.2.2 提交事务1.2.3 回滚&#xff08;撤销&#xff09;事务实例1&#xff1a;一致性实例2&#xff1a;原子性 【 2. 字符集 和 校对规则 】2.1 基本原理2.2 查看字符集查看…

机房网络运维服务项目难点与关键点分析

随着信息技术的飞速发展&#xff0c;机房作为支撑企业信息化建设的核心枢纽&#xff0c;其网络运维服务的重要性日益凸显。然而&#xff0c;在实际运维过程中&#xff0c;运维团队常常面临诸多难点和挑战。本文将围绕机房网络运维服务项目的难点和关键点进行深入分析&#xff0…

【第三节】C/C++数据结构之栈与队列

目录 一、数据结构-栈 1.1 栈的定义 1.2 栈的 ADT (Abstract Data Type) 1.3 栈的顺序存储结构及实现 二、数据结构-队列 2.1 队列的定义 2.2 队列的 ADT 2.3 队列的顺序存储结构与实现 2.4 优先队列 2.5 各种队列异同点 一、数据结构-栈 1.1 栈的定义 栈(Stack)可…

【计算机毕业设计】谷物识别系统Python+人工智能深度学习+TensorFlow+卷积算法网络模型+图像识别

谷物识别系统&#xff0c;本系统使用Python作为主要编程语言&#xff0c;通过TensorFlow搭建ResNet50卷积神经算法网络模型&#xff0c;通过对11种谷物图片数据集&#xff08;‘大米’, ‘小米’, ‘燕麦’, ‘玉米渣’, ‘红豆’, ‘绿豆’, ‘花生仁’, ‘荞麦’, ‘黄豆’, …