ExecutorService - 10个技巧和窍门
ExecutorService已经成为Java并发编程中常用的基础库,几乎所有到线程 任务等执行都要委托ExecutorService。下面是使用过程中10个技巧和窍门。
1.为线程池和线程取名
当我们在调试程序将JVM中线程导出Dump时,会出现pool-N-thread-M这样的提示,这是缺省的线程池名称,其中N代表池的序列号,每次你创建一个新的线程池,这个N数字就增加1;而M是这个线程池中的线程顺序。举例, pool-2-thread-3 表示在第二个线程池中的第三个显式,JDK将这种命名策略封装在ThreadFactory,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. 按照上下文切换名称
一旦我们记住线程的名称,我们就可以在运行时改变它们,这是有意义的,因为线程dump值显示类和方法名称,没有参数和本地变量。通过调整线程名称能实现事务标识,以便我们方便跟踪具体的消息 记录或查询,找出哪个是引起死锁的原因,如下:
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()立即关闭 )——这完全取决于你的用例。 例如如果我们提交了一堆任务,希望尽快返回全部完成后使用 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);
}
这种情况下我们发送大量电子邮件,每个作为在线程池中一个单独的任务。 这些任务提交后,我们关闭池不再接受任何新的任务。 然后我们等待最多一分钟,直到完成所有这些任务。 然而,如果某些任务仍悬而未决,,awaitTermination() 只会返回 假 。 ,等待任务将继续进行处理。
final List<Runnable> rejected = executorService.shutdownNow(); log.debug("Rejected tasks: {}", rejected.size());
这次所有队列中任务将被取消返回,而已经运行的任务将允许继续。
4. 小心处理中断
- 当线程堵塞在某个带有InterruptedException的方法时,你可以在这个线程中调用Thread.interrupt(),大多数堵塞的方法将立即抛出InterruptedException.
- 如果你将任务提交给线程池(ExecutorService.submit()),当认为已经在执行时,你能调用Future.cancel(true),在这种情况下线程池将试图中断线程运行你的任务,你能很有效率的中断你的任务。
5. 监控队列和保持长度有限
不正确大小的线程池可能会导致缓慢、不稳定和内存泄漏。 如果您配置线程太少,将建立队列消耗大量的内存。 另一方面,太多的线程由于过度上下文切换会减慢整个系统,导致相同的症状。 查看队列深度很重要,保持它有界,以便超负荷的线程池可以暂时拒绝新任务:
final BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
executorService = new ThreadPoolExecutor(n, n,
0L, TimeUnit.MILLISECONDS,
queue);
上面代码等同于Executors.newFixedThreadPool(n),但是我们使用有固定容量100的ArrayBlockQueue替代了缺省是无限的LinkedBlockingQueue,这意味着如果有100个任务在队列中,n代表被执行,新的任务将被拒绝加入抛出RejectedExecutionException, 因为队列现在置于外部,我们可以定期地调用其size()放入logs/JMX等监控机制中。
6. 记得Exception处理
下面这段代码会是什么结果?
executorService.submit(() -> { System.out.println(1 / 0); });
它不会打印任何东西。也没有抛出除0的错误,线程池自己吞进了这个exception,好像从来没有发生一样,这是线程池与普通线程的区别,如果你提交一个Runnable,你必须自己使用try-catch环抱方法体,至少记录日志,如果你提交Callable确保你总是能解除引用,使用堵塞get()到re-throw exception:
final Future<Integer> division = executorService.submit(() -> 1 / 0); //下面将抛出由除零ArithmeticException引起的ExecutionException division.get();
有趣的是Spring框架的@Async也有这个bug.
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. 保存客户端跟踪堆栈
反应性编程这些天似乎得到了大量的关注,. Reactive manifesto, reactive streams, RxJava (just released 1.0!),Clojure agents, scala.rx… 他们都工作的很好,但堆栈跟踪不再是你的朋友,他们是最无用的。 比如一个例外发生在任务提交给线程池:
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]
我们可以很容易发现 mytask 在第76行抛出NPE错误。 但是我们不知道谁提交了这个任务,因为堆栈跟踪只显示Thread 和ThreadPoolExecutor 。 在技术上我们可以浏览源代码的希望能找到 mytask 被创建的地方。 但是我们不能立即看到线程全貌(更不用说event-drivent事件驱动或 actor-ninja-编程了)。 如果我们可以保留客户端代码(提交的任务)的堆栈跟踪并显示它,例如在失败时显示,那就是一个好办法,当然这并不是一个新想法, Hazelcast 从所有者节点到客户机传播异常代码 中也是这么做的。自然地保存客户端堆栈可以跟踪错误:
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引入了更强大 CompletableFuture。 请尽可能使用它。 ExecutorService 没有扩展到支持这个增强的抽象,所以你必须自己使用。 下面是没有使用:
final Future<BigDecimal> future = executorService.submit(this::calculate);
使用后效果:
final CompletableFuture<BigDecimal> future = CompletableFuture.supplyAsync(this::calculate, executorService);
虽然CompletableFuture拓展了Future,但更高级的API的消费者会真正欣赏这个扩展功能。
10. 同步队列
SynchronousQueue是一个有趣的 BlockingQueue,但不是真正的队列。 它甚至不是一个数据结构 。 最好是解释为一个容量0的队列。 引用JavaDoc:
每一个 插入 操作必须等待由另一个线程 删除,反之亦然。 同步队列没有任何内部容量,甚至一个也没有。 你不能插入任何一个元素(使用任何方法),除非有另一个线程正在试图删除它,你不能没有迭代遍历。 […]
同步队列类似CSP和Ada的通道。
这和线程池是什么关系,看看下面使用同步队列实现的线程池:
BlockingQueue<Runnable> queue = new SynchronousQueue<>(); ExecutorService executorService = new ThreadPoolExecutor(n, n, 0L, TimeUnit.MILLISECONDS, queue);
当创建带有两个线程的线程池,因为SynchronousQueue 是一个容量为0的队列,因此线程池只有在有空闲线程的情况下接受新任务,如果所有线程很忙,将拒绝新任务,不会有任何等待,这种模式适合后台启动任务需要要么立即执行要么取消不能执行。
ExecutorService - 10个技巧和窍门的更多相关文章
- 45个实用的JavaScript技巧、窍门和最佳实践
在这篇文章中,我将分享一组JavaScript的技巧.窍门和最佳实践,这些都是JavaScript程序员应该知晓的,不管他们是使用在浏览器/引擎上,还是服务器端(SSJS——Service Side ...
- 编写高性能Web应用程序的10个技巧
这篇文章讨论了: ·一般ASP.NET性能的秘密 ·能提高ASP.NET表现的有用的技巧和窍门 ·在ASP.NET中使用数据库的建议 ·ASP.NET中的缓存和后台处理 使用ASP.NET编写一个We ...
- 实用的JavaScript技巧、窍门和最佳实践
JavaScript是世界上第一的编程语言,它是Web的语言,是移动混合应用(mobile hybrid apps)的语言(比如 PhoneGap或者 Appcelerator),是服务器端的语言(比 ...
- 【转】45个实用的JavaScript技巧、窍门和最佳实践
原文:https://colobu.com/2014/09/23/45-Useful-JavaScript-Tips,-Tricks-and-Best-Practices/ 目录 [−] 列表 第一次 ...
- 【转】天啦噜!原来Chrome自带的开发者工具还能这么用!(提升JS调试能力的10个技巧)
天啦噜!原来Chrome自带的开发者工具还能这么用! (提升JS调试能力的10个技巧) Chrome自带开发者工具.它的功能十分丰富,包括元素.网络.安全等等.今天我们主要介绍JavaScript ...
- 让你的 Node.js 应用跑得更快的 10 个技巧(转)
Node.js 受益于它的事件驱动和异步的特征,已经很快了.但是,在现代网络中只是快是不行的.如果你打算用 Node.js 开发你的下一个Web 应用的话,那么你就应该无所不用其极,让你的应用更快,异 ...
- 转自微软内部资料:编写高性能 Web 应用程序的 10 个技巧
编写高性能 Web 应用程序的 10 个技巧 转自微软资料数据层性能技巧 1 — 返回多个结果集技巧 2 — 分页的数据访问技巧 3 — 连接池技巧 4 — ASP.NET 缓存 API技巧 5 — ...
- Flex移动应用程序开发的技巧和窍门(三)
这是关于 Flex 移动应用程序开发的技巧和窍门系列文章的第三部分内容.第一部分内容主要集中讨论了视图之间以及应用程序执行之间切换时的数据处理.第二部分则主要涵盖了应用程序动作条和标签组件风格化方面的 ...
- Flex移动应用程序开发的技巧和窍门(二)
范例文件 flex-mobile-dev-tips-tricks-pt2.zip 这是关于Flex移动应用程序开发的技巧和窍门的一系列文章中的第二部分.第一部分 内容主要集中讨论了视图之间以及应用程序 ...
随机推荐
- 刨根问底U3D---关于Delegate的方方面面
我是否需要阅读这篇文章 Code1: private delegate void BloodNumDelegate (); public delegate void ExpNumChangeDeleg ...
- acm算法模板(2)
数学问题: 1.精度计算——大数阶乘 2.精度计算——乘法(大数乘小数) 3.精度计算——乘法(大数乘大数) 4.精度计算——加法 5.精度计算——减法 6.任意进制转换 7.最大公约数.最小公倍数 ...
- Uva 11538 - Chess Queen
http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&p ...
- Java基础(49):快速排序的Java封装(含原理,完整可运行,结合VisualGo网站更好理解)
快速排序 对冒泡排序的一种改进,若初始记录序列按关键字有序或基本有序,蜕化为冒泡排序.使用的是递归原理,在所有同数量级O(n longn) 的排序方法中,其平均性能最好.就平均时间而言,是目前被认为最 ...
- .NET: C#: System.Diagnostics
1. Trace & Debug 理解这两者的区别,Trace有个Listners.Add()非常好用,这里网上有个在ListBox里输出Debug和Trace信息的 using System ...
- 创建一个web Test Plan
1.添加ThreadGroup (1).线程组界面解析: 线程数:虚拟用户的个数 Ramp-up Period:开启每个用户的延迟时间,如果有5个虚拟用户,Ramp-up Period值是5,Jmet ...
- 。。。JDBC里面的sql与hibernate里面的hql有关占位符"?"的总结。。。
今天在看Hibernate的时候,似乎又有了一些收获的东东,嘻嘻... 我记得很清楚:以前用JDBC操作数据库的时候是这样的: String sql = "select * from use ...
- Java实现数组按数值大小排序
package shb.java.test; /** * 比较数组中元素的大小,按从大到小顺序排列. * @Package:shb.java.test * @Description: * @autho ...
- [tp3.2.1]sql查询语句(一)
基本查询方式 字符串条件查询, 索引数组条件查询 对象条件查询 SQL语句大小写是一样的,但是,执行的时候有一个小写到大写的转换,所以最好写大写 $condition=new ...
- 各种类型的Writable(Text、ByteWritable、NullWritable、ObjectWritable、GenericWritable、ArrayWritable、MapWritable、SortedMapWritable)转
java原生类型 除char类型以外,所有的原生类型都有对应的Writable类,并且通过get和set方法可以他们的值. IntWritable和LongWritable还有对应的变长VIntWri ...