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 manifestoreactive streamsRxJava (just released 1.0!),Clojure agentsscala.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个技巧和窍门的更多相关文章

  1. 45个实用的JavaScript技巧、窍门和最佳实践

    在这篇文章中,我将分享一组JavaScript的技巧.窍门和最佳实践,这些都是JavaScript程序员应该知晓的,不管他们是使用在浏览器/引擎上,还是服务器端(SSJS——Service Side ...

  2. 编写高性能Web应用程序的10个技巧

    这篇文章讨论了: ·一般ASP.NET性能的秘密 ·能提高ASP.NET表现的有用的技巧和窍门 ·在ASP.NET中使用数据库的建议 ·ASP.NET中的缓存和后台处理 使用ASP.NET编写一个We ...

  3. 实用的JavaScript技巧、窍门和最佳实践

    JavaScript是世界上第一的编程语言,它是Web的语言,是移动混合应用(mobile hybrid apps)的语言(比如 PhoneGap或者 Appcelerator),是服务器端的语言(比 ...

  4. 【转】45个实用的JavaScript技巧、窍门和最佳实践

    原文:https://colobu.com/2014/09/23/45-Useful-JavaScript-Tips,-Tricks-and-Best-Practices/ 目录 [−] 列表 第一次 ...

  5. 【转】天啦噜!原来Chrome自带的开发者工具还能这么用!(提升JS调试能力的10个技巧)

    天啦噜!原来Chrome自带的开发者工具还能这么用! (提升JS调试能力的10个技巧)   Chrome自带开发者工具.它的功能十分丰富,包括元素.网络.安全等等.今天我们主要介绍JavaScript ...

  6. 让你的 Node.js 应用跑得更快的 10 个技巧(转)

    Node.js 受益于它的事件驱动和异步的特征,已经很快了.但是,在现代网络中只是快是不行的.如果你打算用 Node.js 开发你的下一个Web 应用的话,那么你就应该无所不用其极,让你的应用更快,异 ...

  7. 转自微软内部资料:编写高性能 Web 应用程序的 10 个技巧

    编写高性能 Web 应用程序的 10 个技巧 转自微软资料数据层性能技巧 1 — 返回多个结果集技巧 2 — 分页的数据访问技巧 3 — 连接池技巧 4 — ASP.NET 缓存 API技巧 5 — ...

  8. Flex移动应用程序开发的技巧和窍门(三)

    这是关于 Flex 移动应用程序开发的技巧和窍门系列文章的第三部分内容.第一部分内容主要集中讨论了视图之间以及应用程序执行之间切换时的数据处理.第二部分则主要涵盖了应用程序动作条和标签组件风格化方面的 ...

  9. Flex移动应用程序开发的技巧和窍门(二)

    范例文件 flex-mobile-dev-tips-tricks-pt2.zip 这是关于Flex移动应用程序开发的技巧和窍门的一系列文章中的第二部分.第一部分 内容主要集中讨论了视图之间以及应用程序 ...

随机推荐

  1. Codeforces Round #288 (Div. 2)

    A. Pasha and Pixels     题意就是给一个n*m的矩阵,k次操作,一开始矩阵全白,一次操作可以染黑一个格子,问第几次操作可以使得矩阵中存在一个2*2的黑色矩阵.直接模拟即可 代码: ...

  2. mybatis(二)执行CRUD操作的两种方式配置和注解

    一.使用MyBatis对表执行CRUD操作——基于XML的实现 1.定义sql映射xml文件 userMapper.xml文件的内容如下: <?xml version="1.0&quo ...

  3. PTPX中的clock tree与LP design

    PTPX在加入CPF/UPF这样的文件后,可以分析multi-voltage,power-gating这样的设计. 针对某个power rail的cell,PTPX支持进行annotate. set_ ...

  4. 【GDI+】一些规则多边形分离的问题

    在近期的工作中,需要做一样工作:将一些有规则的图形,进行适当的分离,以达到不重叠的问题. 首先组成图形的点都可以是按照逆时针排好序的. 规则的图形可以大致分为三类: A :两个点组成的线 或者 四个点 ...

  5. C语言资源

    0.C自带库/函数在线文档 http://www.acm.uiuc.edu/webmonkeys/book/c_guide/ 1.so文件制作和使用 http://blog.csdn.net/love ...

  6. 关于undefined reference to `WSASocketA@24'问题的解决

    关于 Eclipse 开发C++ Socket  ,在开发的过程中 用WinGW 平台编译, 示例server端: #include <winsock2.h> #include <m ...

  7. COM编程之四 引用计数

    [1]客户为什么不应直接控制组件的生命期? 假设一个组件A正在使用另一个组件B,可想组件A(客户)代码中肯定有若干个指向组件B接口的指针. 那么这种情况下,当使用完一个接口而仍然在使用另一个接口时,是 ...

  8. Mac Book 上安装Windows 8 / 10 以后安装 Hyper-v 无法正常使用问题---虚拟化已禁止问题

    Mac book由于工作需要装了双启动,平时工作用 Windows 10, 由于有些老程序无法启动,还得再装一个虚拟机. 起初装 WMWare 开始装 7.1版本,结果没安装完就直接崩溃重启, 网上反 ...

  9. 使用 Delphi Xe 的 TDictionary

    原本一直使用 TList, 将定义的一个个 Record 保存在TList 里面, 为了能把某些对象管理起来, 例如一个类的 n 多实例,可以进行索引.查找.释放等 今天刚看到原来已经有了一个叫 TD ...

  10. 161104、NoSQL数据库:key/value型之levelDB介绍及java实现

    简介:Leveldb是一个google实现的非常高效的kv数据库,能够支持billion级别的数据量了. 在这个数量级别下还有着非常高的性能,主要归功于它的良好的设计.特别是LSM算法.LevelDB ...