ExecutorService-10个要诀和技巧

ExecutorService抽象概念自Java5就已经提出来了,现在是2014年。顺便提醒一下:Java5和Java6都已不被支持,Java7在半年内也将会这样。我提出这个的原因是许多Java程序员仍然不能完全明白ExecutorService到底是怎样工作的。还有很多地方要去学习,今天我会分享一些很少人知道的特性和实践。然而这篇文章仍然是面向中等程序员的,没什么特别高级的地方。

1. Name pool threads

我想强调一点的是,当在运行JVM或调试期间创建线程时,默认的线程池命名规则是pool-N-thread-M,这里N代表线程池的序列数(每一次你创建一个线程池的时候,全局计数N就加1),而M则是某一个线程池的线程序列数。例如,pool-2-thread-3就意味着JVM生命周期中第2线程池的第3线程。具体可以查看:Executors.defaultThreadFactory()。这样不具备描述性,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. Switch names according to context

有一个我从 Supercharged jstack: How to Debug Your Servers at 100mph学到的小技巧。一旦我们记住了线程的名字,那么在任何时刻我们都能够改变它们!这是有道理的,因为线程转储显示了类名和方法名,没有参数和局部变量。通过调整线程名保留一些必要的事务标识符,我们可以很容易追踪某一条运行缓慢或者造成死锁的信息/记录/查询等。例如:

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-WHATEVER-MESSAGE-ID-IS,当通过系统追踪信息流时这可能会派上用场。

3. Explicit and safe shutdown

在客户端线程和线程池之间有一个任务队列,当你的应用关闭时,你必须关心两件事:任务队列会发生什么;正在运行的任务会怎样(这个时候将详细介绍)。令人感到吃惊的是许多程序员并不会适当地或有意识地关闭线程池。这有两个方法:要么让所有的任务队列全都执行完(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);
}

在这个例子中我们发送了一堆e-mail,每一个都作为一个独立的任务交给线程池。在提交了所有的任务之后我们执行shutdown使线程池不再接收新的任务。然后最多等待1minute直到所有的任务都完成。然而如果有些任务仍然处于挂起状态,awaitTermination()将返回false,而那些在等待的任务会继续执行。我知道一些人会使用新潮的用法:

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

你可能会觉得我太保守,但我喜欢去控制并行线程的数量。不用介意,还有一种优雅的shutdown()方法shutdownNow():

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

这样一来队列中还在等待的任务将会被舍弃并被返回,但已经在运行的任务将会继续。

4. Handle interruption with care

很少人知道Future