How to Stop Execution After a Certain Time in Java – 在Java中如何在一定时间后停止执行

最后修改: 2020年 10月 6日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we’ll learn how we can end a long-running execution after a certain time. We’ll explore the various solutions to this problem. Also, we’ll cover some of their pitfalls.

在这篇文章中,我们将学习如何在一定时间后结束一个长期运行的执行。我们将探讨这个问题的各种解决方案。此外,我们还将介绍它们的一些陷阱。

2. Using a Loop

2.使用一个循环

Imagine that we’re processing a bunch of items in a loop, such as some details of the product items in an e-commerce application, but that it may not be necessary to complete all the items.

想象一下,我们在一个循环中处理一堆项目,比如电子商务应用中产品项目的一些细节,但可能没有必要完成所有的项目。

In fact, we’d want to process only up to a certain time, and after that, we want to stop the execution and show whatever the list has processed up to that time.

事实上,我们只想处理到某一时间,之后,我们想停止执行,并显示列表中到该时间为止所处理的内容。

Let’s see a quick example:

让我们看一个快速的例子。

long start = System.currentTimeMillis();
long end = start + 30 * 1000;
while (System.currentTimeMillis() < end) {
    // Some expensive operation on the item.
}

Here, the loop will break if the time has surpassed the limit of 30 seconds. There are some noteworthy points in the above solution:

这里,如果时间超过了30秒的限制,循环就会中断。在上述解决方案中,有一些值得注意的地方。

  • Low accuracy: The loop can run for longer than the imposed time limit. This will depend on the time each iteration may take. For example, if each iteration may take up to 7 seconds, then the total time can go up to 35 seconds, which is around 17% longer than the desired time limit of 30 seconds
  • Blocking: Such processing in the main thread may not be a good idea as it’ll block it for a long time. Instead, these operations should be decoupled from the main thread

In the next section, we’ll discuss how the interrupt-based approach eliminates these limitations.

在下一节,我们将讨论基于中断的方法如何消除这些限制。

3. Using an Interrupt Mechanism

3.使用中断机制

Here, we’ll use a separate thread to perform the long-running operations. The main thread will send an interrupt signal to the worker thread on timeout.

在这里,我们将使用一个单独的线程来执行长期运行的操作。主线程将在超时时向工作线程发送一个中断信号。

If the worker thread is still alive, it’ll catch the signal and stop its execution. If the worker finishes before the timeout, it’ll have no impact on the worker thread.

如果工作线程还活着,它就会捕捉到该信号并停止执行。如果工作者在超时前完成,就不会对工作者线程产生影响。

Let’s take a look at the worker thread:

让我们来看看工人线程。

class LongRunningTask implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < Long.MAX_VALUE; i++) {
            if(Thread.interrupted()) {
                return;
            }
        }
    }
}

Here, the for loop through Long.MAX_VALUE simulates a long-running operation. Instead of this, there could be any other operation. It’s important to check the interrupt flag because not all the operations are interruptible. So in those cases, we should manually check the flag.

这里,通过Long.MAX_VALUE的for循环模拟了一个长期运行的操作。取而代之的是,可以有任何其他操作。检查中断标志是很重要的,因为不是所有的操作都可以中断。所以在这些情况下,我们应该手动检查该标志。

Also, we should check this flag in every iteration to ensure that the thread stops executing itself within the delay of one iteration at most.

另外,我们应该在每次迭代中检查这个标志,以确保线程在最多一次迭代的延迟内停止执行自己。

Next, we’ll cover three different mechanisms of sending the interrupt signal.

接下来,我们将介绍发送中断信号的三种不同机制。

3.1. Using a Timer

3.1.使用Timer

Alternatively, we can create a TimerTask to interrupt the worker thread upon timeout:

另外,我们可以创建一个TimerTask来在超时时中断工作线程。

class TimeOutTask extends TimerTask {
    private Thread thread;
    private Timer timer;

    public TimeOutTask(Thread thread, Timer timer) {
        this.thread = thread;
        this.timer = timer;
    }

    @Override
    public void run() {
        if(thread != null && thread.isAlive()) {
            thread.interrupt();
            timer.cancel();
        }
    }
}

Here, we’ve defined a TimerTask that takes a worker thread at the time of its creation. It’ll interrupt the worker thread upon the invocation of its run method. The Timer will trigger the TimerTask after a three seconds delay:

在这里,我们定义了一个TimerTask,它在创建时需要一个工作线程。它将在调用其run方法时中断工人线程。Timer将在延迟三秒后触发TimerTask

Thread thread = new Thread(new LongRunningTask());
thread.start();

Timer timer = new Timer();
TimeOutTask timeOutTask = new TimeOutTask(thread, timer);
timer.schedule(timeOutTask, 3000);

3.2. Using the Method Future#get

3.2.使用方法Future#get

We can also use the get method of a Future instead of using a Timer:

我们也可以使用Futureget方法,而不是使用Timer

ExecutorService executor = Executors.newSingleThreadExecutor();
Future future = executor.submit(new LongRunningTask());
try {
    future.get(7, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    future.cancel(true);
} catch (Exception e) {
    // handle other exceptions
} finally {
    executor.shutdownNow();
}

Here, we used the ExecutorService to submit the worker thread that returns an instance of Future, whose get method will block the main thread until the specified time. It’ll raise a TimeoutException after the specified timeout. In the catch block, we are interrupting the worker thread by calling the cancel method on the Future object.

在这里,我们使用ExecutorService来提交工作线程,它返回一个Future的实例,其get方法将阻塞主线程,直到指定时间。它将在指定的超时后引发一个TimeoutException。在catch块中,我们通过调用Future对象的cancel方法来中断工人线程。

The main benefit of this approach over the previous one is that it uses a pool to manage the thread, while the Timer uses only a single thread (no pool).

与前一种方法相比,这种方法的主要好处是,它使用一个池来管理线程,而Timer只使用一个单线程(没有池)

3.3. Using a ScheduledExcecutorSercvice

3.3.使用ScheduledExcecutorSercvice

We can also use ScheduledExecutorService to interrupt the task. This class is an extension of an ExecutorService and provides the same functionality with the addition of several methods that deal with the scheduling of execution. This can execute the given task after a certain delay of set time units:

我们还可以使用ScheduledExecutorService来中断任务。这个类是ExecutorService的扩展,提供了相同的功能,并增加了几个处理执行调度的方法。这可以在设定的时间单位的某个延迟后执行给定的任务。

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
Future future = executor.submit(new LongRunningTask());
Runnable cancelTask = () -> future.cancel(true);

executor.schedule(cancelTask, 3000, TimeUnit.MILLISECONDS);
executor.shutdown();

Here, we created a scheduled thread pool of size two with the method newScheduledThreadPool. The ScheduledExecutorService#schedule method takes a Runnable, a delay value, and the unit of the delay.

在这里,我们用newScheduledThreadPool方法创建了一个大小为2的调度线程池。ScheduledExecutorService#schedule方法接收一个Runnable,一个延迟值,以及延迟的单位。

The above program schedules the task to execute after three seconds from the time of submission. This task will cancel the original long-running task.

上述程序将任务安排在提交时间的三秒后执行。这个任务将取消原来长期运行的任务。

Note that, unlike the previous approach, we are not blocking the main thread by calling the Future#get method. Therefore, it’s the most preferred approach among all the above-mentioned approaches.

请注意,与前面的方法不同,我们没有通过调用Future#get方法来阻塞主线程。因此,它是上述所有方法中最受欢迎的方法

4. Is There a Guarantee?

4.是否有保证?

There’s no guarantee that the execution is stopped after a certain time. The main reason is that not all blocking methods are interruptible. In fact, there are only a few well-defined methods that are interruptible. So, if a thread is interrupted and a flag is set, nothing else will happen until it reaches one of these interruptible methods.

不能保证在一定时间后停止执行。主要原因是,并非所有的阻塞方法都是可中断的。事实上,只有少数定义明确的方法是可中断的。所以,如果一个线程被中断了,并且设置了一个标志,那么在它到达这些可中断的方法之一之前,不会有其他事情发生

For example, read and write methods are interruptible only if they’re invoked on streams created with an InterruptibleChannel. BufferedReader is not an InterruptibleChannel. So, if the thread uses it to read a file, calling interrupt() on this thread blocked in the read method has no effect.

例如,方法只有在它们被调用到用InterruptibleChannel创建的流上时才是可中断的。BufferedReader并不是一个InterruptibleChannel。因此,如果线程使用它来读取文件,在read方法中阻塞的这个线程上调用interrupt()没有效果。

However, we can explicitly check for the interrupt flag after every read in a loop. This will give a reasonable surety to stop the thread with some delay. But, this doesn’t guarantee to stop the thread after a strict time, because we don’t know how much time a read operation can take.

然而,我们可以在循环中每次读完后明确地检查中断标志。这将提供一个合理的保证,使线程在一定的延迟下停止。但是,这并不能保证在一个严格的时间后停止线程,因为我们不知道一个读操作能花多少时间。

On the other hand, the wait method of the Object class is interruptible. Thus, the thread blocked in the wait method will immediately throw an InterruptedException after the interrupt flag is set.

另一方面,Object类的wait方法是可中断的。因此,在wait方法中阻塞的线程将在中断标志被设置后立即抛出一个InterruptedException

We can identify the blocking methods by looking for a throws InterruptedException in their method signatures.

我们可以通过寻找方法签名中的throws InterruptedException来识别阻塞方法。

One important piece of advice is to avoid using the deprecated Thread.stop() method. Stopping the thread causes it to unlock all of the monitors that it has locked. This happens because of the ThreadDeath exception that propagates up the stack.

一个重要的建议是避免使用被废弃的Thread.stop() 方法。停止线程会导致它解锁它所锁定的所有监视器。这是因为ThreadDeath异常会在堆栈中向上传播而发生。

If any of the objects previously protected by these monitors were in an inconsistent state, the inconsistent objects become visible to other threads. This can lead to arbitrary behavior that is very hard to detect and reason about.

如果之前被这些监视器保护的任何对象处于不一致的状态,不一致的对象就会对其他线程可见。这可能会导致很难检测和推理的任意行为。

5. Design for Interruption

5.中断的设计

In the previous section, we highlighted the importance of having interruptible methods to stop the execution as soon as possible. Therefore, our code needs to consider this expectation from a design perspective.

在上一节中,我们强调了拥有可中断方法以尽快停止执行的重要性。因此,我们的代码需要从设计的角度考虑这种期望。

Imagine we have a long-running task to execute, and we need to make sure it doesn’t take more time than the specified. Also, suppose the task can be split into individual steps.

想象一下,我们有一个长期运行的任务要执行,我们需要确保它的时间不会超过指定的时间。另外,假设该任务可以被分割成各个步骤。

Let’s create a class for the task steps:

让我们为任务步骤创建一个类。

class Step {
    private static int MAX = Integer.MAX_VALUE/2;
    int number;

    public Step(int number) {
        this.number = number;
    }

    public void perform() throws InterruptedException {
        Random rnd = new Random();
        int target = rnd.nextInt(MAX);
        while (rnd.nextInt(MAX) != target) {
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
        }
    }
}

Here, the Step#perform method tries to find a target random integer while asking for the flag on each iteration. The method throws an InterruptedException when the flag is activated.

在这里,Step#perform方法试图找到一个目标随机整数,同时在每次迭代中询问该标志。当标志被激活时,该方法抛出一个InterruptedException

Now, let’s define the task which will be performing all the steps:

现在,让我们定义将执行所有步骤的任务。

public class SteppedTask implements Runnable {
    private List<Step> steps;

    public SteppedTask(List<Step> steps) {
        this.steps = steps;
    }

    @Override
    public void run() {
        for (Step step : steps) {
            try {
                step.perform();
            } catch (InterruptedException e) {
                // handle interruption exception
                return;
            }
        }
    }
}

Here, the SteppedTask has a list of steps to execute. A for loop performs each step and handles the InterruptedException for stopping the task when it occurs.

这里,SteppedTask有一个要执行的步骤列表。一个for循环执行每个步骤,并处理InterruptedException,以便在发生时停止任务。

Finally, let’s see an example of using our interruptible task:

最后,让我们看一个使用我们的可中断任务的例子。

List<Step> steps = Stream.of(
  new Step(1),
  new Step(2),
  new Step(3),
  new Step(4))
.collect(Collectors.toList());

Thread thread = new Thread(new SteppedTask(steps));
thread.start();

Timer timer = new Timer();
TimeOutTask timeOutTask = new TimeOutTask(thread, timer);
timer.schedule(timeOutTask, 10000);

First, we create a SteppedTask with four steps. Second, we run the task using a thread. Last, we interrupt the thread after ten seconds using a timer and a timeout task.

首先,我们创建一个有四个步骤的SteppedTask。其次,我们使用一个线程来运行该任务。最后,我们使用一个定时器和一个超时任务在10秒后中断该线程。

With this design, we can ensure our long-running task can be interrupted while executing any step. As we have seen before, the downside is there is no guarantee it will stop at the exact time specified, but surely better than a non-interruptible task.

通过这种设计,我们可以确保我们的长期运行的任务在执行任何步骤时都可以被中断。正如我们之前所看到的,缺点是不能保证它在指定的确切时间停止,但肯定比不可中断的任务好。

6. Conclusion

6.结语

In this tutorial, we’ve learned various techniques for stopping the execution after a given time, along with the pros and cons of each. The complete source code can be found over on GitHub.

在本教程中,我们已经学习了在给定时间后停止执行的各种技术,以及每种技术的利弊。完整的源代码可以在GitHub上找到over。