Timeout a callable by another thread or ExecutorService awaitTermination. In the previous post, here, we discussed how to have a runnable or callable with timeout using ExecutorService. In this post, we optimize the previous solution to be more robust.
When the get
method of a future
is executed in the main thread, it will get blocked. The main thread has to wait until the future finishes execution or times out. This could be a bad thing in many scenarios.
To avoid that we need to delegate the cancellation task to another thread as it’s discussed below.
Delegating timeout task to another thread
In this approach, we need to schedule a new thread that will start after a certain time and calls of.cancel(true)
our callable/runnable. For simplicity sake, let’s call it watcher
.
To have our watcher working, we need to switch the executor service implementation from newSingleThreadExecutor()
to newScheduledThreadPool(2)
. The final implementation will be something like below snippet:
import java.util.concurrent.*;
public class Service {
private final ScheduledExecutorService executorService;
public Service() {
executorService = Executors.newScheduledThreadPool(2);
}
public void testExecutorServiceInterrputCallable() {
setTimeout(executorService.submit(() -> someBlockingTask()), 1);
System.out.println("Continue main thread!");
shutdownExecutorService();
}
private void setTimeout(Future<?> futureTask, int timeoutInSeconds) {
executorService.schedule(() -> futureTask.cancel(true), timeoutInSeconds, TimeUnit.SECONDS);
}
private void someBlockingTask() {
for (long i = 0; i <= 10000000; i++) {
System.out.println(i);
if (Thread.currentThread().isInterrupted()) { // 3
System.out.println("The callable has been interrupted!");
return;
}
}
}
private void shutdownExecutorService() {
executorService.shutdown();
if (!executorService.isShutdown()) {
executorService.shutdownNow();
}
}
}
As you can see, the only changes are in line 10 and 20. In line 10 we created two threads instead of one. The first one is responsible to run someBlockingTask
and the second one is scheduled to cancel the first thread after a certain time. Line 20, is the implementation of our watcher, second thread, that is scheduled to run after 1 second to call.cancel(true)
method of the first thread.
The caveat of this approach is if the main thread finishes its execution, it does not wait for the scheduler. Hence, the JVM shutdowns while the task is still running.
One way to mitigate this problem is to use the executor service timeout which is discussed below. Keep in mind that this approach still blocks the main thread but compare with the first example in the previous post is a much better solution.
Leveraging on awaitTermination
of the ExecutorService
Executorservice provides awaitTermination
method that acts as a timeout clock. It is used with a combination ofshutdown
and shutdownNow
methods.
But before diving further, let’s focus on the tasks of shutdown
and shutdownNow
methods.
shutdown
results in executor service to not accept new tasks but still continues execution of the current tasks in hand.shutdownNow
results in executor service to terminate immediately no matter any tasks are running or not.
The implementation would be something like below:
import java.util.concurrent.*;
public class Service {
private final ExecutorService executorService;
public Service() {
executorService = Executors.newFixedThreadPool(1);
}
public void testExecutorServiceInterrputCallable() {
setTimeout(executorService.submit(() -> someBlockingTask()), 1);
System.out.println("Continue main thread!");
shutdownExecutorService();
}
private void setTimeout(Future<?> futureTask, int timeoutInSeconds) {
executorService.shutdown();
try {
if (!executorService.awaitTermination(timeoutInSeconds, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException ie) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
private void someBlockingTask() {
for (long i = 0; i <= 10000000; i++) {
System.out.println(i);
if (Thread.currentThread().isInterrupted()) { // 3
System.out.println("The callable has been interrupted!");
return;
}
}
}
private void shutdownExecutorService() {
executorService.shutdown();
if (!executorService.isShutdown()) {
executorService.shutdownNow();
}
}
}
The only difference between the above example and the other example from the previous post is the implementation of setTimeout
method.
As you can see, first we call shutdown
method so the executor will not accept a new task, even though in this example there is only a single task. Then we wait for one second and after that call shutdownNow()
which basically interrupts the task and the task will stop as it’s coded, see line 34.
This approach is a more robust solution compare with the previous one because it blocks the main thread. You just need to keep in mind to not call setTimeout
method after creating a method. In the example, for simplicity sake, setTimeout
is called after scheduling the task. In a real-world scenario, this should not happen.