Tasks scheduled inside an application may often need to be cancelled. This can be because of several reasons such as,
- Task failed to complete in a certain time and is now useless
- Application is shutting down and all the existing tasks should be cancelled
- Application is running low on memory and killing lower priority tasks is one way to free up memory
- User requested cancellation for a task
Everything inside a Java application runs inside a thread. This may be the main thread, a user created thread, or a daemon thread. Thus, cancelling a tasks requires us to signal its executing thread to stop task execution. This signalling is where interruptions come into play.
But before we talk more about interruptions, let us try to brainstorm a solution on our own to fix this problem.
Naive Solution for Task Cancellation
Let us first create a runnable to model our task. This is how it may look.
public class Task implements Runnable {
@Override
public void run() {
while(true) {
executeTask();
}
}
void executeTask() {
// Task execution logic here
}
}
This is an endless task that keeps on running forever. We would like to add support for cancelling this task at some later point in time. How can we achieve this?
Let us introduce a state to our task that will indicate if we need to stop executing task. Here is how our class will now look like.
public class Task implements Runnable {
private volatile boolean cancelled = false;
@Override
public void run() {
while(!cancelled) {
executeTask();
}
}
public void cancel() {
cancelled = true;
}
void executeTask() {
// Task execution logic here
}
}
We continually check the cancelled flag to determine if task execution should be cancelled. Once the task is marked cancelled, it will exit in the very next iteration. While this appear to solve our problem, in reality it does not!
Let us consider a case where executeTask method makes a call to a blocking method.
public class Task implements Runnable {
private volatile boolean cancelled = false;
private final BlockingQueue<Object> blockingQueue;
public Task(BlockingQueue<Object> blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
while(!cancelled) {
executeTask();
}
}
public void cancel() {
cancelled = true;
}
void executeTask() {
try {
Object taskToExecute = blockingQueue.take(); // Blocking call
// Task execution logic here
} catch (InterruptedException e) {
// Ignore any Interrupted exception
}
}
}
Now what if our task thread is blocked on this blocking method call? Despite us setting the cancelled flag to true, our task will never exit. It will keep on waiting for a new element to arrive in the blocking queue.
Thus, our naive solution fails this very possible use-case! We therefore need some better way to cancel tasks.
Interruptions
Interruption is a cooperative mechanism via which one thread can signal another thread. While a thread can interpret this signal in any possible way, it is almost always understood as a request to ‘stop what you are doing and exit as soon as possible’.
Each thread has an associated boolean variable to represent interrupted status. Interrupting a thread sets this variable to true.
Following are some methods in Thread class that are related to interruption mechanism.
public class Thread {
public void interrupt() { ... }
public boolean isInterrupted() { ... }
public static boolean interrupted() { ... }
...
}
- interrupt(): Sets the interrupted status of a thread to true.
- isInterrupted(): Returns a boolean value representing the interrupted status of the thread.
- interrupted(): Returns a boolean value representing the interrupted status of the thread and additionally clearing the interrupted status.
But, you may ask, how does interruption solves the issue of task cancellation during blocking calls?
Most of the blocking methods are responsive to interruptions. If they detect that a waiting thread has been interrupted, they exit early by throwing an InterruptedException.
Note that blocking methods while throwing an InterruptedException also clears the interrupted status of the executing thread.
Also, it is the duty of the executing activity to periodically poll the interrupted status of the thread when it is not blocked. The executing activity, on detecting interruption, should then take necessary actions.
Let us fix our Task class using interruptions.
public class Task implements Runnable {
private volatile boolean cancelled = false;
private final BlockingQueue<Object> blockingQueue;
public Task(BlockingQueue<Object> blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
try {
while(!Thread.currentThread().isInterrupted()) {
executeTask();
}
} catch (InterruptedException e) {
// Thread was interrupted
// Allow it to exit
}
}
public void cancel() {
cancelled = true;
}
void executeTask() throws InterruptedException {
Object taskToExecute = blockingQueue.take();
// Task execution logic here
}
}
Well behaved methods may totally ignore interruption requests as long as they keep the interrupted status intact. Poorly behaved methods swallows interrupt request thus denying code further up the call stack the opportunity to act on it.
Interruption Policies
Cancellation policy describes cleanups performed by a task when an interruption is detected. Similar to a the cancellation policy is the thread interruption policy. It describes how a thread behaves on receiving an interrupt signal. For most common use cases, it is simply ‘exit the thread and notify the owning entity’.
Most often, tasks are executed in threads that are owned by separate services (ex. Executor Service). In such a case, tasks are unaware of the interruption policy a thread follows. It is thus very important to communicate the interrupt status from task to the thread. Now this may be achieved by throwing an InterruptedException or by preserving the interrupted status of the thread. But, we may never swallow the interrupt status of a thread in a task!
Unfortunately, our Task class’ run method swallows the interrupted status. We can fix it in the following way.
class Task {
...
@Override
public void run() {
try {
while(!Thread.currentThread().isInterrupted()) {
executeTask();
}
} catch (InterruptedException e) {
// Thread was interrupted
// Allow it to exit and restore the interrupted status
Thread.currentThread().interrupt();
}
}
...
}
Bonus: Timed Run
Let us look at an example of running a task for a certain amount of time. If the task failed to complete in the specified amount of time, we would want to cancel it.
We will use the Future interface to implement a solution for this. Future interface is used to denote the results of an asynchronous task and can also be used to cancel tasks. It provides a method called cancel.
The cancel method works in the following way,
- Cancelling won’t work: If the task is already done, cancelled before, or can’t be cancelled for some reason, trying to cancel it again has no effect.
- Stopping a task before it starts: If you call cancel() on a task that hasn't begun yet, it will never even start running.
- Interrupting a running task: For tasks that have already started, the mayInterruptIfRunning parameter controls how we try to stop them. It decides if the thread running the task should be interrupted during the cancellation attempt.
Here is the code that would run a task for a certain amount of time before cancelling it.
public static void timedRun(Runnable r, long timeout, TimeUnit unit) {
Future<?> task = taskExec.submit(r); // Submit Returns a Future
try {
task.get(timeout, unit);
} catch (TimeoutException e) {
// task will be cancelled below
} catch (ExecutionException e) {
// exception thrown in task; rethrow
throw e;
} finally {
// Harmless if the result has already been computed
task.cancel(true);
}
}
With that we reach the end of this blog. I hope you enjoyed the content and learned something new today.
If you are interested in more such articles on Java Concurrency, here are some of my favourites that I encourage you to read!
- Object Sharing in Multi-Threaded environment
- Visibility Across Threads
- Thread Safety from an Object Oriented Perspective
- Java’s Synchronized Collections
- A Nice Race Condition
- Produce Consumer Pattern Using Java’s Blocking Queues
- Java’s Executor Framework
- How to achieve thread Harmony: A guide to synchronizers
Top comments (0)