JUC — ExecutorService线程必备优化手段

ExecutorService已经成为Java并发编程中常用的基础库,几乎所有用到线程 任务等执行都要委托给ExecutorService处理,下面介绍ExecutorService在开发中的优化手段:

1.为线程池和线程取名

当我们在调试程序从JVM线程中导出Dump时,会出现pool-N-thread-M这样的提示,这是缺省的线程池名称,其中N代表池的序列号,每次你创建一个新的线程池,这个N数字就增加1;而M是这个线程池中的线程顺序。举例, pool-2-thread-3 表示在第二个线程池中的第三个线程,JDK将这种命名策略封装在ThreadFactory,线程命名代码示例:

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() 只会返回false ,将继续等待任务处理。

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

这次所有队列中任务将被取消返回,而已经运行的任务将允许继续。

4. 小心处理interrupt

当线程堵塞在某个带有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()到exception:

final Future division = executorService.submit(() -> 1 / 0);
//下面get()将抛出由除零引起的ArithmeticException
division.get();

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 响应式编程似乎得到了大量的关注,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事件驱动编程了)。 如果我们可以保留客户端代码(提交的任务)的堆栈跟踪并显示它,例如在失败时显示,那就是一个好办法,当然这并不是一个新想法, 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 thread 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:

每一个 插入 操作必须等待由另一个线程 删除,反之亦然。 同步队列没有任何内部容量,
甚至一个也没有。 你不能插入任何一个元素(使用任何方法),除非有另一个线程正在试图
删除它,你不能没有迭代遍历。 […]

这和线程池是什么关系,看看下面使用SynchronousQueue 同步队列实现的线程池:

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

当创建带有两个线程的线程池,因为SynchronousQueue 是一个容量为0的队列,因此线程池只有在有空闲线程的情况下接受新任务,如果所有线程很忙,将拒绝新任务,不会有任何等待,这种模式适合后台启动任务,要么立即执行要么不能执行。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RachelHwang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值