【问题标题】:Single Thread Executor Silently Drops Tasks单线程执行器静默丢弃任务
【发布时间】:2017-07-03 08:06:16
【问题描述】:

我正在努力解决一个问题,在一天的大部分时间里顺利工作之后,一个可调用的任务被放入 Java 单线程执行器中,并且显然永远不会被执行。提交新任务的后续调用失败,ExecutorService 似乎已死。此时,产生任务的客户端停止服务,直到可以重新启动流程,这在工作时间是不可能的。

一些背景: 多个高吞吐量生产者线程将他们的任务放在他们自己专用的Single Thread ExecutorService 上并立即返回。低延迟对于生产者线程非常重要。生产者线程和执行者线程之间存在一对一的关系。需要按顺序为每个生产者线程处理任务。任务可以在执行器线程中排队,只要它们需要执行就可以。流量是突发的,所以消费者总是赶上他们的生产者。

JDK:RedHat Linux 上的 jdk1.8.0_92

我定义了我的执行器服务:

private final ExecutorService inboundMsgSender = Executors.newSingleThreadExecutor();

生产者线程调用回调:

public void onMessageFromFix(MessageEvent event, final Message message) {
    log.info("submit to Executor: " + message.toString());
    inboundMsgSender.submit(new Callable<Void>() {
        public Void call() {
            try {
                onMessageFromExecutor(event, message);
            } catch (Throwable e) {
                log.error("error", e);
            }
            return null;
        }
    });
}

ExecutorService 调用可调用对象:

    public void onMessageFromExecutor(MessageEvent event, final Message message) {
    try {
        log.info("call from Executor: " + message.toString());
        doExpensiveLogic(message);
    } catch (Exception e) {
        log.error("error", e);
    }
}

正常情况下我在日志文件中看到:

submit to Executor: 4928

call from Executor: 4928

这就是我知道 Executor 线程正在运行 Callable 的方式。

当问题发生时,我只看到以下内容:

submit to Executor: 4928

没有后续的call from Executor,也没有异常。

【问题讨论】:

  • 尝试在尝试之前、onMess 之后放置日志消息...这将缩小搜索范围
  • 什么调用 onMessageFromFix? inboundMsgSender.submit 是否有可能抛出调用者正在吞下的异常?
  • 调用由第三方 API 调用。是的,有可能正在吞噬一个异常。我认为这是最可能的原因。我想将 Callable 更改为 Runnable,因为我等不及 get() 重新抛出任何异常。我听说使用 Runnable 会在我的 try/catch 中捕获异常,但我不确定。
  • 如果您认为这是最可能的原因,那么在提交时放置一个 try-catch,应该会告诉您。我认为如果您使用 runnable 或 callable 并没有太大区别,但我认为在您的情况下,runnable 更干净一些,因为您既没有返回结果也没有抛出检查的异常。
  • 迈克尔,也抓住 Throwable。

标签: java multithreading


【解决方案1】:

从未执行可调用任务的原因是因为inboundMsgSender Single Thread ExecutorService 内的线程被阻塞,等待上一次调用的`doExpensiveLogic(message) 内的FutureTask.get()

这里的教训是,我假设 ExecutorService 的线程在它刚刚阻塞时就快死了。线程死亡由 ExecutorService 处理,所以我等待问题再次发生,然后使用 JStack 进行了线程转储。线程转储准确显示了执行器服务的线程被阻塞的位置。

"pool-54-thread-1" #354 prio=5 os_prio=0 tid=0x567c3c00 nid=0xae4a waiting on condition [0x51125000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x69458368> (a com.aqua.api.SequentialExecutorService$ClientTaskHandle)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
    at java.util.concurrent.FutureTask.get(FutureTask.java:191)
    at com.aqua.jms.multiserver.impl.MultiServerJmsConnection.isConsumerConfigured(MultiServerJmsConnection.java:301)
    at com.aqua.jms.multiserver.migration.MigrationConnectionWrapper.getAdministrationConnection(MigrationConnectionWrapper.java:152)

当它再次发生时我采取的步骤:

  1. 标识执行器服务单线程的线程名。
  2. 在 linux 上,识别进程的 PID。
  3. 使用 jstack 获取 PID 的线程转储 $ jstack 33516 > threaddump.txt
  4. 在线程转储中搜索线程名称(见上文)。

您可以从堆栈跟踪中清楚地看到,线程在 FutureTask.get() 上是 LIVE 和 WAITING,因此需要做的就是修复 Future Task 或重构其中的逻辑并使其可用于我的线程直接调用。

【讨论】:

  • 您的线程是否因未捕获的异常而死亡?我不认为 Java 会无缘无故地杀死长时间运行的线程。
  • 线程没有死。线程转储清楚地显示它正在等待 FutureTask.get()。
  • 所以实际上执行者并没有放弃你的任务。它按预期工作。您可以优化昂贵的操作或允许多个线程处理任务。
  • 我必须先解决死锁。有一个 FutureTask,它的 get() 方法永远不会返回。线程转储不显示块发生的位置。只有 Callable 永远不会完成。我无法在测试中重现该问题。但我认为问题在于过于复杂的自定义执行器服务。我计划用标准的 Executor Service 替换它,并希望最好。
  • CompletableFuture 可以帮助您而无需等待 future.get() :docs.oracle.com/javase/8/docs/api/java/util/concurrent/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-30
  • 1970-01-01
  • 1970-01-01
  • 2016-02-21
  • 2013-07-22
  • 2019-08-26
相关资源
最近更新 更多