When we submit a task (Callable or its cousin Runnable) for execution to an Executor or ExecutorService (e.g. ThreadPoolExecutor), we get a Future back which wraps the tasks. It has a method called
Future.cancel(...)which can be used to cancel the task the future wraps. Calling this method has different effects depending on the stage the task is in. A task could be in one of three stages:
- The task hasn’t started executing yet - it’s waiting in the work queue for a thread to start executing it.
- A thread is executing the task.
- The task has finished executing.
Cancellation is trivial, if the task hasn’t started executed. It is simply removed from the work queue. Similarly, if the task has finished executing, cancelling it has no effect.
It is a little more involved when the task is currently executing. To interrupt a task whose handle we have obtained using Future, we can use use
Future.cancel(...). From Javadocs:
boolean cancel(boolean mayInterruptIfRunning)
Attempts to cancel execution of this task. This attempt will fail if the task has already completed, has already been cancelled, or could not be cancelled for some other reason. If successful, and this task has not started when cancel is called, this task should never run. If the task has already started, then the mayInterruptIfRunning parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task.</div>
However cancel might not always work if the task is running. This is because to stop threads in Java, we rely on a co-operative mechanism called Interruption. The idea is very simple. To stop a thread, we deliver it an interrupt signal, requesting that it stops itself at the next available opportunity. If the thread cooperates, it will stop. If it doesn’t, it will ignore the interrupt request completely and carry on as if nothing happened.
Future.cancel(...) delivers an interrupt signal to the thread asking it to stop. You must ensure that your tasks respect the interrupt signals e.g. checks for
Thread.currentThread().isInterrupted() at regular intervals.
Sometimes, it’s necessary to support non-standard task cancellation. It is useful when you are dealing with 3rd party libraries that don’t respond or respect interruptions or if the task relies on long-running blocking methods. For example, ServerSocket.accept method waits for a client connection when invoked and ignores all interruptions request while it’s in the wait state.
To support nonstandard cancellation when interrupts don’t work, there are two ways of doing it. In both approaches, you will have to do something creative to cancel the task that’s ignoring interrupts. For example, in the case of
ServerSocket.accept(), we could close the underlying socket which will throw an exception, forcing
accept(...) out of the wait state.
Approach #1. Override Thread.interrupt()
- Provide a custom ThreadFactory to the ExecutorService.
- Return custom Threads which override the interrupt method.
Note: Overriding interrupt() method is not recommended and you should use this carefully.
Approach #2. Override Future.cancel(…)
In your tasks (e.g.
Callable) provide a method for non-standard cancellation, such as
cancelTask(). Then override the
Future.cancel(...) to call this method.
We do not normally create a Future ourselves and specify what the
cancel(...) method does: we get Future when we submit a task via ExecutorService.submit(…). But there is a way. Under the hood, ExecutorService calls on a method newTaskFor(Callable c) to obtain Futures. We can override this method to return a custom Future which overrides the
cancel(...) method. This is shown below with an example:
Whereas the IdentifiableCallable is shown below:
Next we need to define our own FutureWrapper so we can override the cancel(…) method:
Now we need to define our task as follows:
That’s all. Now when we call
FutureTaskWrapper.cancel(...), it will in turn call cancelTask(), where we can define our non-standard cancellation.
The entire code used in this post is available on GitHub.