ExecutorService的十个使用技巧

[url=http://www.oracle.com/technetwork/java/eol-135779.html]ExecutorService](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html)这个接口从Java 5开始就已经存在了。这得追溯到2004年了。这里小小地提醒一下,官方已经不再支持Java 5, Java 6了,Java 7[在半年后也将停止支持[/url]。我之所以会提起ExecutorService这么旧的一个接口是因为,大多数Java程序员并没有搞清楚它的工作原理。关于它可以介绍的有很多,这里我只想分享它的一些较少为人所知的特性以及实践技巧。本文主要是面向初级程序员的,并没有过于高深的东西。


1. 线程命名

这点得反复强调。对正在运行的JVM进行线程转储(thread dump)或者调试时,线程池默认的命名机制是pool-N-thread-M,这里N是线程池的序号(每新创建一个线程池,这个N都会加一),而M是池里线程的序号。比方说,pool-2-thread-3指的是JVM生命周期中第二个线程池里的第三个线程。参考这里 [url=https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadFactory.html]Executors.defaultThreadFactory()](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#defaultThreadFactory--)。这样的名字表述性不佳。由于JDK将命名机制都隐藏在[ThreadFactory[/url]里面,这使得要正确地命名线程得稍微费点工夫。所幸的是Guava提供了这么一个工具类:


import com.google.common.util.concurrent.ThreadFactoryBuilder;

final ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("Orders-%d")
.setDaemon(true)
.build();
final ExecutorService executorService = Executors.newFixedThreadPool(10, threadFactory);


2. 根据上下文切换名字

这是我从[url=http://www.javacodegeeks.com/2014/10/supercharged-jstack-how-to-debug-your-servers-at-100mph.html]高效的jstack:如何对高速运行的服务器进行调试[/url]一文中学到的一个技巧。线程名可以随时进行修改,只要你想这么做的话。这是有一定的意义的,因为线程转储只能看到类名和方法名,而没有参数及本地变量。通过调整线程名可以保留一些比较关键的上下文信息,这样排查消息/记录/查询等变慢或者出现死锁的问题时就容易多了。示例:



private void process(String messageId) {
executorService.submit(() -> {
final Thread currentThread = Thread.currentThread();
final String oldName = currentThread.getName();
currentThread.setName("Processing-" + messageId);
try {
//real logic here...
} finally {
currentThread.setName(oldName);
}
});
}



在try-finally块中当前线程的名字是Processing-某个消息ID。这对跟踪系统内的消息流会比较有用。

3. 显式地安全地关闭线程

客户端线程和线程池之间会有一个任务队列。当程序要关闭时,你需要注意两件事情:入队的这些任务的情况怎么样了以及正在运行的这个任务执行得如何了。令人惊讶的是很多开发人员并没能正确地或者有意识地去关闭线程池。正确的方法有两种:一个是让所有的入队任务都执行完毕(shutdown()),再就是舍弃这些任务(shutdownNow())——这完全取决于你。比如说如果我们提交了N多任务并且希望等它们都执行完后才返回的话,那么就使用shutdown():



private void sendAllEmails(List<String> emails) throws InterruptedException {
emails.forEach(email ->
executorService.submit(() ->
sendEmail(email)));
executorService.shutdown();
final boolean done = executorService.awaitTermination(1, TimeUnit.MINUTES);
log.debug("All e-mails were sent so far? {}", done);
}




本例中我们发送了许多电子邮件,每一封邮件都对应着线程池中的一个任务。提交完这些任务后我们会关闭线程池,这样就不会再有新的任务进来了。然后我们会至少等待一分钟,直到这些任务执行完。如果1分钟后还是有的任务没执行到的话,awaitTermination()便会返回false。但是剩下的任务还会继续执行。我知道有些赶时髦的人会这么写:

emails.parallelStream().forEach(this::sendEmail);



他们觉得我那样很老套,不过我个人比较喜欢能控制并发线程的数量。还有一个优雅地关闭掉线程池的方法就是shutdownNow():



final List<Runnable> rejected = executorService.shutdownNow();
log.debug("Rejected tasks: {}", rejected.size());



这么做的话队列中的所有任务都会被舍弃并返回。已执行的任务仍会继续执行。

4. 谨慎地处理中断

Future的一个较少提及的特性便是cancelling。这里我就不重复多说了,可以看下我之前的一篇文章:[url=http://www.nurkiewicz.com/2014/05/interruptedexception-and-interrupting.html]InterruptedException及线程中断[/url]。

5. 监控队列长度,确保队列有界

不当的线程池大小会使得处理速度变慢,稳定性下降,并且导致内存泄露。如果配置的线程过少,则队列会持续变大,消耗过多内存。而过多的线程又会由于频繁的上下文切换导致整个系统的速度变缓——殊途而同归。队列的长度至关重要,它必须得是有界的,这样如果线程池不堪重负了它可以暂时拒绝掉新的请求:


final BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
executorService = new ThreadPoolExecutor(n, n,
0L, TimeUnit.MILLISECONDS,
queue);



上面的代码等价于Executors.newFixedThreadPool(n),然而不同的是默认的实现是一个无界的LinkedBlockingQueue。这里我们用的是一个固定100大小的ArrayBlockingQueue。也就是说如果已经有100个任务在队列中了(还有N个在执行中),新的任务就会被拒绝掉,并抛出RejectedExecutionException异常。由于这里的队列是在外部声明的,我们还可以时不时地调用下它的size()方法来将队列大小记录在到日志/JMX/或者你所使用的监控系统中。

6. 别忘了异常处理

下面这段代码执行的结果是什么?


executorService.submit(() -> {
System.out.println(1 / 0);
});



我被它坑过无数回了:它什么也不会输出。没有任何的java.lang.ArithmeticException: / by zero的征兆,啥也没有。线程池会把这个异常吞掉,就像什么也没发生过一样。如果是你自己创建的java.lang.Thread还好,这样[url=https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.UncaughtExceptionHandler.html]UncaughtExceptionHandler[/url]还能起作用。不过如果是线程池的话你就得小心了。如果你提交的是Runnable对象的话(就像上面那个一样,没有返回值),你得将整个方法体用try-catch包起来,至少打印一下异常。如果你提交的是Callable<Integer>的话,得确保你在用get()方法取值的时候重新抛出异常:



final Future<Integer> division = executorService.submit(() -> 1 / 0);
//below will throw ExecutionException caused by ArithmeticException
division.get();



有趣的是Spring框架的@Async为此还弄出了个BUG,参见:[url=https://jira.spring.io/browse/SPR-12090]SPR-8995](https://jira.spring.io/browse/SPR-8995)以及 [SPR-12090[/url]。

7. 监控队列中的等待时间

监控工作队列的长度只是一个方面。然而排除故障时查看从提交任务到实际执行之间的时间差就显得非常重要了。这个时间差越接近0就越好(说明正好线程池中有空闲的线程),否则任务要入队的话这个时间就会增加了。再进一步说,如果线程池不是固定线程数的话,执行新的任务还得新创建一个线程,这个同样也会消耗一定的时间。为了能更好地监控这项指标,可以对ExecutorService做一下封装:



public class WaitTimeMonitoringExecutorService implements ExecutorService {

private final ExecutorService target;

public WaitTimeMonitoringExecutorService(ExecutorService target) {
this.target = target;
}

@Override
public <T> Future<T> submit(Callable<T> task) {
final long startTime = System.currentTimeMillis();
return target.submit(() -> {
final long queueDuration = System.currentTimeMillis() - startTime;
log.debug("Task {} spent {}ms in queue", task, queueDuration);
return task.call();
}
);
}

@Override
public <T> Future<T> submit(Runnable task, T result) {
return submit(() -> {
task.run();
return result;
});
}

@Override
public Future<?> submit(Runnable task) {
return submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
task.run();
return null;
}
});
}

//...

}



这个实现并不完整,不过也能说明大概的意思了。当我们将任务提交给线程池的时候,便立即开始记录它的时间。一旦这个任务被取出并开始执行时便停止计时。不要被代码中的startTime和queueDuration这两个变量搞混了。事实上它们是在两个不同的线程中进行求值的,通常都会差个毫秒级或者秒级:



Task com.nurkiewicz.MyTask@7c7f3894 spent 9883ms in queue



8. 保留客户端的栈跟踪信息

近来响应式编程受到了不少关注。 [url=https://github.com/lihaoyi/scala.rx]Reactive manifesto](http://www.reactivemanifesto.org/), [reactive streams](http://www.reactive-streams.org/), [RxJava](https://github.com/ReactiveX/RxJava)(仅发布了1.0版本!),[Clojure agents](http://clojure.org/agents), [scala.rx[/url]等等。它们都非常不错,但栈跟踪信息就完蛋了,它们几乎是毫无价值的。假设提交到线程池中的一个任务出现了异常:


java.lang.NullPointerException: null
at com.nurkiewicz.MyTask.call(Main.java:76) ~[classes/:na]
at com.nurkiewicz.MyTask.call(Main.java:72) ~[classes/:na]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) ~[na:1.8.0]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) ~[na:1.8.0]
at java.lang.Thread.run(Thread.java:744) ~[na:1.8.0]



可以很容易发现NPE异常出现在MyTask的76行。但是我们并不知道是谁提交的这个任务,因为栈信息只能看到Thread以及ThreadPoolExecutor。技术上来讲我们当然是可以看下代码,看看是何处创建的MyTask。不过如果没有线程在这中间的话,我们马上便能知道是谁提交的任务。那么如果我们可以保留客户端代码(提交任务的那段代码)的栈信息呢?这个想法并非我首创的,[url=https://github.com/hazelcast/hazelcast/blob/7f8cd30e4e445473271d2e434ad939d156a151ca/hazelcast/src/main/java/com/hazelcast/util/ExceptionUtil.java#L29]Hazelcast](http://hazelcast.com/)就将[异常从所有者节点传播到了客户端中[/url]。下面是一个非常简单的将客户端栈信息保留下来以便失败时查看的例子:


public class ExecutorServiceWithClientTrace implements ExecutorService {

protected final ExecutorService target;

public ExecutorServiceWithClientTrace(ExecutorService target) {
this.target = target;
}

@Override
public <T> Future<T> submit(Callable<T> task) {
return target.submit(wrap(task, clientTrace(), Thread.currentThread().getName()));
}

private <T> Callable<T> wrap(final Callable<T> task, final Exception clientStack, String clientThreadName) {
return () -> {
try {
return task.call();
} catch (Exception e) {
log.error("Exception {} in task submitted from thrad {} here:", e, clientThreadName, clientStack);
throw e;
}
};
}

private Exception clientTrace() {
return new Exception("Client stack trace");
}

@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
return tasks.stream().map(this::submit).collect(toList());
}

//...

}



这样一旦失败的话我们便可以取到完整的栈信息以及提交任务时所在的线程的名字。跟之前相比我们有了一些更有价值的信息:


Exception java.lang.NullPointerException in task submitted from thrad main here:
java.lang.Exception: Client stack trace
at com.nurkiewicz.ExecutorServiceWithClientTrace.clientTrace(ExecutorServiceWithClientTrace.java:43) ~[classes/:na]
at com.nurkiewicz.ExecutorServiceWithClientTrace.submit(ExecutorServiceWithClientTrace.java:28) ~[classes/:na]
at com.nurkiewicz.Main.main(Main.java:31) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0]
at java.lang.reflect.Method.invoke(Method.java:483) ~[na:1.8.0]
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134) ~[idea_rt.jar:na]




9. 优先使用CompletableFuture


Java 8中引入了更为强大的[url=http://www.javacodegeeks.com/2013/05/java-8-definitive-guide-to-completablefuture.html]CompletableFuture[/url]。有可能的话尽量使用下它。ExecutorService并没有扩展以支持这个增强型的接口,因此你得自己动手了。这么写是不行的了:



final Future<BigDecimal> future =
executorService.submit(this::calculate);



你得这样:



final CompletableFuture<BigDecimal> future =
CompletableFuture.supplyAsync(this::calculate, executorService);



CompletableFuture 继承自Future,因此跟之前的用法一样。但是使用你接口的人一定会感谢CompletableFuture所提供的这些额外的功能的。


10. 同步队列

[url=https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/SynchronousQueue.html]SynchronousQueue[/url]是一个非常有意思的BlockingQueue。它本身甚至都算不上是一个数据结构。最好的解释就是它是一个容量为0的队列。这里引用下Java文档中的一段话:

[i]
每一个insert操作都需要等待另一个线程的一个对应的remove操作,反之亦然。同步队列内部不会有任何空间,甚至连一个位置也没有。你无法对同步队列执行peek操作,因为仅当你要移除一个元素的时候才存在这么个元素;如果没有别的线程在尝试移除一个元素你也无法往里面插入元素;你也无法对它进行遍历,因为它什么都没有。。。

同步队列与CSP和Ada中所用到的集结管道(rendezvous channel)有异曲同工之妙。
[/i]

它和线程池有什么关系?你可以试试在ThreadPoolExecutor中用下SynchronousQueue:


BlockingQueue<Runnable> queue = new SynchronousQueue<>();
ExecutorService executorService = new ThreadPoolExecutor(n, n,
0L, TimeUnit.MILLISECONDS,
queue);


我们创建了一个拥有两个线程的线程池,以及一个SynchronousQueue。由于SynchronousQueue本质上是一个容量为0的队列,因此这个ExecutorService只有当有空闲线程的时候才能接受新的任务。如果所有的线程都在忙,新的任务便会马上被拒绝掉,不会进行等待。这在要么立即执行,要么马上丢弃的后台执行的场景中会非常有用。

终于讲完了,希望你能找到一个自己感兴趣的特性!


原创文章转载请注明出处:[url=http://it.deepinmind.com/java/2014/11/26/executorservice-10-tips-and-tricks.html]http://it.deepinmind.com[/url]


[url=http://www.javacodegeeks.com/2014/11/executorservice-10-tips-and-tricks.html]英文原文链接[/url]
UVM环境复位--stop_sequences() 在UVM验证环境中, stop_sequences是一个用于强制终止所有正在运行的 Sequence 的方法,通常用于动态控制激励生成流程。1.通过uvm_config_db传递事件和严格的同步机制,此方案符适用于复杂复位场景和动态测试控制。组件耦合:Driver 和 Sequence 不再直接依赖env,通过标准接口通信。竞争条件:确保 Driver 完成清理后再终止 Sequence。代码健壮性:使用非阻塞操作和事件监听,避免死锁。 阅读详情

相关推荐

接入 deepseek 实现AI智能问诊

必须包含user角色,用于传递用户输入。system角色可选,用于设置对话背景。多轮对话需要将历史记录添加到messages中。

weixin_51326478的博客 6541

接口ExecutorService的方法使用

1.ExecutorService接口继承于Executor接口(线程池最顶层父类),抽象类AbstractExecutorService实现了ExecutorService接口,最常用的线程池类ThreadPoolExecutor就是继承于AbstractExecutorService抽象类。 2.接口ExecutorService 中有很多工具方法,ExecutorService接口中的方法有些与 Future 和Callable有关,所以 Future与Callable 的使用是学习 Executor

weixin_40472874的博客 731

C++跨平台Word文档处理库DuckX实战指南:从原理到项目集成

在C++开发中,处理Office文档,尤其是Word文件,常因缺乏原生易用的库而面临挑战。Office Open XML(OOXML)作为.docx文件的基础标准,其本质是包含XML文件的ZIP压缩包。理解这一原理,开发者便能通过解析和操作XML来实现文档的读写。DuckX库正是基于此,为C++提供了轻量级的解决方案。它封装了底层的ZIP处理和XML解析,提供了简洁的面向对象API,其技术价值在于实现了跨平台、无外部依赖的Word文档自动化生成与处理。这尤其适用于服务器端、嵌入式环境或需要脱离Office软

weixin_33508411的博客 387

Executor, ExecutorService 和 Executors 间的不同

出处: https://yemengying.com/2017/03/17/difference-between-executor-executorService/java.util.concurrent.Executor, java.util.concurrent.ExecutorService, java.util.concurrent. Executors 这三者均是 Java Execut

橙子wj的博客 1062

AC360变频器使用案例

AC360变频器使用案例!

QING_BOSS的博客 517

Camera(5)metadata

Metadata 是整个 Android camera 中类似于高楼大厦中的管道系统一样,贯穿了整个 Camera 的 Framework 和 vendor HAL。Metadata 里面包含了所有的控制、参数、返回值等等。Metadata分为Android原生的Metadata和芯片商或者用户自定义的Metadata,即Android Tag和Vendor Tag。Vendor Tag在Hal中定义,并在camx和chi目录中使用。

qq_55893245的博客 3050

Java并发编程4——Executor / ExecutorService / ThreadPoolExecutor线程池

继承关系 Executor接口(顶层接口) ExecutorService接口 AbstractExecutorService抽象类 ThreadPoolExecutor实现类 ThreadPoolExecutor 属性 线程池状态(ctl属性) 构造器 / 参数 线程池运行机制 源码级别 Work类 Executors工具类提供的线程池实现 nexFixedThreadPool:固定大小的线程池 newCachedThreadPool:带缓冲池.

qq_41157876的博客 457

《最游记》游戏开发全栈指南:从源码解析到实战部署

本文提供了一份详尽的《最游记》游戏源码全栈开发指南。文章深入解析了这款3D玄幻网游的完整源码结构,涵盖客户端、服务端、引擎和工具链,并提供了从环境搭建、核心模块解析到实战修改与部署的完整流程。对于希望深入理解MMORPG架构的开发者而言,这份指南是宝贵的实战资源。

weixin_28363123的博客 71

Executors创建ExecutorService(线程池)的常用方法

Executors创建ExecutorService(线程池)的常用方法

weixin_42594143的博客 1293

Halcon实例转OpenCVSharp--粘连物体分割与计数(基于距离变换+分水岭算法) (附源码)

本文作者Color Space,文章未经作者允许禁止转载! 本文将介绍OpenCVSharp应用实例--粘连物体分割与计数(基于距离变换+分水岭算法)! 本文使用的OpenCVSharp版本---》v4.5.3。 实现目标:将下图中的硬币分割出来并计算数量。 预期效果图: ...

Color Space的博客 2419

7、基于机器学习的生产效率估计方法解析

本文探讨了基于机器学习的生产效率估计方法,重点介绍了效率分析树(EAT)和凸效率分析树(CEAT)算法的原理及其与传统自由处置壳(FDH)和数据包络分析(DEA)方法的对比。通过剪枝优化、生产可能性集构建及方向距离函数(DDF)估计,EAT和CEAT在避免过拟合、提升估计稳定性与处理凸性假设方面展现出优势。结合PISA教育数据的实证研究,采用箱线图、核密度图和非参数检验方法验证了不同模型效率得分分布的显著差异。研究表明,EAT与CEAT为生产效率评估提供了更准确、灵活的工具,适用于教育、企业生产和医疗等多个

milk5的博客 92

【JUC线程池】ExecutorService以及Executors线程池类型

线程池创建concurrent是Java的并发包。我认为当我们了解关于Java的多线程,就应该知道线程池从何而来:是为了解决在的环境下,对于下所以才会有线程池的概念。所以第一部就要先了解如何创建线程池。线程池创建初次了解线程池肯定会了解到ThreadPoolExecutor类,其内部有四种有参构造方法均是为了创建线程池。接下来主要示范的是七个形参的构造方法。:如果创建的线程数未达到指定的核心线程数,则会继续创建新的核心线程数达到指定值,而后创建的线程均不为核心线程,而是非核心线程。

m0_65563175的博客 948

2026华为OD面试题029:勇攀数字高峰

在给定的数字地形图里寻找登山路径,数字代表当前位置的海拔高度。要求从最低海拔出发,不断攀登,最终到达最高山峰。地图保证最低海拔和最高山峰都只有一个。 登山规则: - 路径上的海拔必须严格递增 - 只能向上下左右 4 个方向移动 - 路径必须从最低海拔开始,到最高海拔结束 - 每个地点只能走一次 - 每一步的攀登高度差必须大于 0,且不超过指定值 `K` 输出满足条件的登山路径的数量。

琅嬛福地 115

钉钉集成工具类封装:HTTP、Token、回调验签与限流

企业系统对接开放平台,本质是围绕 HTTP 客户端、凭证生命周期、安全回调与流量治理做一层稳定 SDK。以钉钉集成为例,access_token 需懒加载、提前过期、并发刷新并支持多实例共享;回调则要完成验签、AES 解密和幂等去重;消息下发还要配合限流、退避重试与日志脱敏。把这些通用能力封装成协议层、凭证层、能力层和辅助层工具类,能把网络重试、token 失效、重复回调、接口限流等工程问题收口,减少业务代码里的复制粘贴。审批、考勤、告警、工单和自动化通知等场景都可在其上快速拼装,核心在于先搭好钉钉开放平台

weixin_34342905的博客 345

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

ExecutorService-10个要诀和技巧

  ExecutorService-10个要诀和技巧 2016/06/16 | 分类: 基础技术 | 3 条评论 | 标签: EXECUTORSERVICE 分享到: 译文出处: simonwang   原文出处:Tomasz Nurkiewicz ExecutorService抽象概念自Java5就已经提出来了,现在是2014年。顺便提醒一下:Java5和Java6都已不被支持,Jav...

GoodIdea 219

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

ExecutorService - 10个技巧和窍门

ExecutorService已经成为Java并发编程中常用的基础库,几乎所有到线程 任务等执行都要委托ExecutorService。下面是使用过程中10个技巧和窍门。 1.为线程池和线程取名   当我们在调试程序将JVM中线程导出Dump时,会出现pool-N-thread-M这样的提示,这是缺省的线程池名称,其中N代表池的序列号,每次你创建一个新的线程池,这个N数字就增加1;而...

ddak56357的博客 160

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

ExecutorService-10个要诀和技巧【转】

http://ifeve.com/executorservice-10-tips-and-tricks/

weixin_34216107的博客 89
上一篇: 编程语言简史
下一篇: 切勿自作聪明,慎用双括号反模式
spidercoco
博客等级 码龄18年 47粉丝 0原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值