【问题标题】:ScheduledFuture.get() is still blocked after Executor shutdownExecutor 关闭后 ScheduledFuture.get() 仍然被阻塞
【发布时间】:2015-03-22 20:52:23
【问题描述】:

我想在后台运行一些周期性任务,我想把它做对。
所以我用ScheduledExecutorService.scheduleWithFixedDelay(..) 安排我的任务并在单独的线程上调用ScheduledFuture.get() 来控制任务的生命周期,从中捕获未捕获的异常并在任务被取消时得到通知。

问题在于,如果在任务执行时调用ScheduledExecutorService.shutdown(),则ScheduledFuture 不会收到通知,并且其get() 方法将永远处于阻塞状态。

下面是说明问题的简单代码:

public final class SomeService {
    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    private final SomePeriodicTask task = new SomePeriodicTask();
    private ScheduledFuture<?> future;

    public void start() {
        future = executor.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);

        final Runnable watchdog = new Runnable() {
            @Override
            public void run() {
                try {
                    future.get();
                } catch (CancellationException ex) {
                    System.out.println("I am cancelled");
                } catch (InterruptedException e) {
                    System.out.println("I am interrupted");
                } catch (ExecutionException e) {
                    System.out.println("I have an exception");
                }
                System.out.println("Watchdog thread is exiting");
            }
        };
        new Thread(watchdog).start();
    }

    public void shutdownAndWait() {
        System.out.println("Shutdown requested");
        executor.shutdown();
        try {
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
        } catch (InterruptedException e) { //BTW When and why could this happen?
            System.out.println("Waiting for shutdown was interrupted");
        }
        System.out.println("Executor is shutdown " + executor.isShutdown());
    }
}

第一,快速返回的简单任务

final class SomePeriodicTask implements Runnable {
    @Override
    public void run() {
        System.out.print("I am just doing my job...");
        System.out.println("done");
    }
}

如果我们像这样运行它

public static void main(String[] args) throws InterruptedException {
    SomeService service = new SomeService();
    service.start();
    Thread.sleep(3000);
    service.shutdownAndWait();
    System.out.println("Future is cancelled " + service.future.isCancelled());
    System.out.println("Future is done " + service.future.isDone());
}

那么输出就是

I am just doing my job...done
I am just doing my job...done
I am just doing my job...done
Shutdown requested
I am cancelled
Watchdog thread is exiting
Executor is shutdown true
Future is cancelled true
Future is done true

完全符合预期。

但是如果我们修改任务来模拟一些繁重的工作

final class SomePeriodicTask implements Runnable{
    @Override
    public void run() {
        System.out.print("I am just doing my job...");
        try {
            Thread.sleep(1500); //Heavy job. You can change it to 5000 to be sure
        } catch (InterruptedException e) {
            System.out.println("Task was interrupted");
        }
        System.out.println("done");
    }
}

以便在任务执行时调用shutdown()...那么输出是

I am just doing my job...done
I am just doing my job...Shutdown requested
done
Executor is shutdown true
Future is cancelled false
Future is done false

所以这里发生了什么......执行程序正在关闭,正如预期的那样。它让当前任务按预期完成其工作。我们看到 executor 确实完成了关闭,但我们的 ScheduledFuture 没有被取消,它的 get() 方法仍然被阻塞,并且看门狗线程阻止 JVM 退出并永远挂起。

当然有解决方法。例如,我可以在关闭之前手动调用future.cancel(false) 或使watchdog 成为守护线程,甚至尝试自行安排关闭Executor 以使其不与正在运行的任务重叠......但以上所有内容都有缺点,当代码会变得更复杂的事情可以横盘整理。

无论如何,我正在寻求您的专家意见,因为在我明白为什么它的行为不应该如此之前,我将无法平静。如果是 jdk 中的错误,则必须报告。如果我误解了某些东西并且我的代码有错误,我必须知道它......

提前致谢

【问题讨论】:

    标签: java multithreading concurrency


    【解决方案1】:

    首先要了解的是,对于周期性 任务,正常完成不会将Future 的状态转变为done,因为它预计会被重新安排并可能重新运行。

    这会干扰Executor.shutdown 的语义;它将cancel 所有待处理 任务,但让当前正在运行的任务完成。因此,您当前正在运行的任务正常完成,并且不会将其状态设置为 done,因为它从未这样做过,但不会因为执行程序已关闭而重新安排。

    即使您使用shutdownNow,它也会中断当前正在运行的任务但不会取消它们,并且由于您的任务捕获InterruptedException 并提前完成但通常情况下,不会有状态转换到done

    添加取消即使在关机时正常完成的任务的所需行为的最佳位置是Executor 实现;换行就行了

    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    

    private final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1) {
      @Override
      protected void afterExecute(Runnable r, Throwable t) {
        if(t==null && isShutdown() && r instanceof RunnableScheduledFuture<?>)
        {
          RunnableScheduledFuture<?> rsf = (RunnableScheduledFuture<?>)r;
          if(rsf.isPeriodic()) rsf.cancel(false);
        }
      };
    };
    

    【讨论】:

    • @Holder 非常感谢您的帮助,但它不能回答我的问题。问题是为什么 ScheduledFuture.get() 会无限期地被阻止,即使 Executor 清楚且正确地关闭并且很久以前就消失了。它应该返回 null 或抛出某种异常或任何你命名的异常,但不能永远挂在那里。这种行为没有正确记录,在我看来确实是错误的和错误的。我倾向于认为它是一个错误。我打印未来的状态只是为了冗长,以表明未来没有收到任何关闭通知。我不在乎它的实际......
    • ...状态。可能它令人困惑,我应该把它排除在外。您的解决方案又是一种解决方法。我在问题中提到了其中三个,而您提出了第四个。肯定还有更多,但我仍然想知道为什么ScheduledFuture.get() 在关闭时会被致命阻止,以及为什么它被认为是正确的行为。
    • 确实解释了这种行为。 是否关心未来的状态并不重要,但未来关心。只要未来没有完成,get 方法就永远不会返回。周期性的未来没有完成。点。
    • 如果您不想要变通方法,请不要创建“看门狗”。并且永远不要在周期性任务的未来调用get,因为很明显它永远无法正常完成。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-20
    相关资源
    最近更新 更多