`
deepinmind
  • 浏览: 452204 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
1dc14e59-7bdf-33ab-841a-02d087aed982
Java函数式编程
浏览量:41679
社区版块
存档分类
最新评论

ExecutorService的十个使用技巧

阅读更多


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


1. 线程命名

这点得反复强调。对正在运行的JVM进行线程转储(thread dump)或者调试时,线程池默认的命名机制是pool-N-thread-M,这里N是线程池的序号(每新创建一个线程池,这个N都会加一),而M是池里线程的序号。比方说,pool-2-thread-3指的是JVM生命周期中第二个线程池里的第三个线程。参考这里 Executors.defaultThreadFactory()](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#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. 根据上下文切换名字

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


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。这里我就不重复多说了,可以看下我之前的一篇文章:InterruptedException及线程中断

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还好,这样UncaughtExceptionHandler还能起作用。不过如果是线程池的话你就得小心了。如果你提交的是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,参见:SPR-8995](https://jira.spring.io/browse/SPR-8995)以及 [SPR-12090

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](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等等。它们都非常不错,但栈跟踪信息就完蛋了,它们几乎是毫无价值的。假设提交到线程池中的一个任务出现了异常:

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。不过如果没有线程在这中间的话,我们马上便能知道是谁提交的任务。那么如果我们可以保留客户端代码(提交任务的那段代码)的栈信息呢?这个想法并非我首创的,Hazelcast](http://hazelcast.com/)就将[异常从所有者节点传播到了客户端中。下面是一个非常简单的将客户端栈信息保留下来以便失败时查看的例子:

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,因此跟之前的用法一样。但是使用你接口的人一定会感谢CompletableFuture所提供的这些额外的功能的。


10. 同步队列

SynchronousQueue是一个非常有意思的BlockingQueue。它本身甚至都算不上是一个数据结构。最好的解释就是它是一个容量为0的队列。这里引用下Java文档中的一段话:


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

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


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

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


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

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


原创文章转载请注明出处:http://it.deepinmind.com



英文原文链接
4
3
分享到:
评论

相关推荐

    ExecutorService10个要诀和技巧编程开发技术

    ExecutorService10个要诀和技巧编程开发技术共9页.pdf.zip

    java编程技巧典型案例解析

    - 线程的创建与执行:Thread类的使用,Runnable接口实现,以及ExecutorService的使用。 - 同步机制:synchronized关键字,Lock接口,以及死锁、活锁的概念。 - 线程池:ThreadPoolExecutor的配置与使用,以及...

    Java实际开发中的小技巧汇总

    以下是一些从博客文章"Java实际开发中的小技巧汇总"中可能涵盖的知识点,结合“源码”和“工具”的标签,我们可以探讨以下几个方面: 1. **异常处理**:在Java中,正确地捕获和处理异常是非常关键的。避免在catch块...

    编程技巧:如何使用线程

    综上所述,"编程技巧:如何使用线程"这份资料将涵盖线程的创建、同步、通信、异常处理和线程池的使用等多个关键知识点,为开发者提供全面的多线程编程指导。通过深入学习和实践,开发者可以更好地驾驭多线程,提升...

    android开发技巧整理

    理解线程池的工作原理,使用ExecutorService可以更高效地管理并发任务。 5. **性能监控**:使用Android Studio内置的Profiler工具,实时监控CPU、内存、网络等性能指标,找出性能瓶颈。学会分析Systrace,了解系统...

    Android 开发技巧新整理.pdf

    Android开发技巧新整理的知识点涵盖了Android应用开发的多个方面,包括UI布局、网络通信、数据存储、组件使用等。从内容概要中,我们可以提炼出以下知识点: 1. 开发环境搭建与调试技巧: - 如何搭建Android开发...

    java面试技巧及笔试汇集

    3. 线程池:掌握ExecutorService、ThreadPoolExecutor、ScheduledExecutorService等线程池的使用。 4. 线程安全:了解并发编程中的volatile、Atomic类和ThreadLocal。 六、网络编程 1. Socket通信:理解TCP和UDP...

    java编程小技巧共享

    10. **异常链**:当捕获一个异常时,如果需要保留原始异常信息,可以通过`throw new Exception("message", e)`来创建异常链。 11. **equals与hashCode**:当重写equals方法时,也应同时重写hashCode方法,以保持...

    JAVA笔试题&面试技巧

    - **线程池**:理解ExecutorService,ThreadPoolExecutor和ScheduledExecutorService的使用。 4. **IO流**: - **字节流与字符流**:理解两者之间的区别,以及BufferedReader和BufferedWriter的作用。 - **NIO...

    184个Android开发常见问题、实用技巧及注意事项

    这份资料“184个Android开发常见问题、实用技巧及注意事项”正是为了帮助开发者解决这些问题而准备的。以下是部分可能涵盖的知识点: 1. **Activity生命周期**:理解Activity的创建、启动、暂停、恢复和销毁过程,...

    Java性能优化技巧集锦

    6. **并发编程**:理解synchronized、volatile和Atomic类的用法,合理使用线程池(ExecutorService)以减少线程创建的开销。掌握锁的优化策略,如读写锁(ReentrantReadWriteLock)。 7. **避免冗余计算**:对计算...

    Java编程技巧典型案例解析

    《Java编程技巧典型案例解析》一书聚焦于这些关键技巧,通过24个精心挑选的源代码案例,深入浅出地展示了Java编程中的精华所在。以下是根据标题、描述以及压缩包子文件的文件名称列表,提炼出的一些核心Java编程知识...

    JAVA编程技巧 原代码

    1. **代码优化**:JAVA编程技巧中的一个重要部分就是代码优化,这涉及到减少冗余代码,提高执行效率,如合理使用`StringBuilder`而非`String`进行字符串拼接,以及避免过度使用`if-else`结构,转而使用策略模式或...

    编程技巧:C/C++,ASP.NET,JAVA,Linux shell,SQL等

    一个重要的技巧是理解和运用STL(Standard Template Library),它包括容器(如vector、list、set)、迭代器和算法,可以提高代码的复用性和效率。另外,深拷贝和浅拷贝的概念对于防止内存泄漏和理解对象生命周期至...

    java android业务异步编程小技巧

    5. **FutureTask**和`ExecutorService`:这是Java并发库的一部分,适用于管理多个异步任务。ExecutorService可以创建线程池,FutureTask则表示异步计算的结果,可以检查任务是否完成,获取或取消结果。 6. **RXJava...

    JAVA开发之学习代码技巧基础.pdf

    以下是一些基础且重要的Java代码技巧,它们涵盖了编程风格、错误处理、内存管理、性能优化等多个方面: 1. **良好的编程规范**:遵循Java编程规范,如命名规则(类名首字母大写,变量名小写等),使用有意义的...

    JAVA程序设计技巧1001例

    读者可以学习到线程的创建和管理,synchronized关键字的使用,以及高级并发工具如ExecutorService和Future的概念。 总之,《JAVA程序设计技巧1001例》全面覆盖了Java编程的各个方面,从基础到进阶,每个实例都是...

    java程序设计技巧1001例

    《Java程序设计技巧1001例》是一个深入学习Java编程的教程,涵盖了广泛的实践案例,旨在提升读者的编程能力和技巧。本教程的核心是通过大量的实例来解释和演示Java编程中的关键概念和技术,帮助读者更好地理解和应用...

Global site tag (gtag.js) - Google Analytics