1. Overview
In Java, there is no safe way to preemptively stop a task running on a Thread in that the task must cooperate and be responsive to the cancellation requests. In this tutorial, we'll examine how we can cancel a task running on a dedicated thread or in a thread pool.
2. Use Cancellation Flag
We'll first look at how we can cancel a task using a cancellation flag. In general, the task checks a boolean flag during its execution. If the flag is set, it cleans up and exits immediately. If we want the task to be more responsive to the cancellation requests, we must occasionally perform this check.
Let's see an example:
public class CancelTaskWithFlag {
private volatile boolean shouldStop = false;
public void startAndCancelAll() throws InterruptedException {
final int threadCount = 5;
final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
final Runnable task = () -> {
while (!shouldStop) {
// Do work.
}
System.out.println("Stopping.");
};
executorService.execute(task);
}
TimeUnit.SECONDS.sleep(1); // Wait for some time
cancelAll();
executorService.shutdown();
}
public void cancelAll() {
shouldStop = true;
}
}
Here, we have two methods: startAndCancelAll and cancelAll. In startAndCancelAll, we're first creating a thread pool. Then we're defining our Runnable task. Note that our task contains a while loop that does the actual work. It runs as long as the shouldStop variable is false. In other words, shouldStop is our cancellation flag. After submitting the tasks, we're invoking cancelAll which sets the flag as true. The tasks that observe the change will exit the loop and stop.
There are some important details in this example.
Firstly, we marked shouldStop as volatile since multiple threads will access it. volatile doesn't provide mutual exclusion but it provides memory visibility. In other words, all threads are guaranteed to see the latest changes made by the other threads.
Secondly, all tasks are using the same flag. So we don't have a way to cancel a specific task.
3. Use Task-Private Cancellation Flag
Next, we'll store the cancellation flag in the task itself. This way we can cancel a specific task instead of all. For this approach to work, we must keep a reference to the submitted task:
public class CancellableTaskWithFlag {
public void startAndCancel() throws InterruptedException {
final ExecutorService executorService = Executors.newFixedThreadPool(5);
final CancellableTask cancellableTask = new CancellableTask();
executorService.execute(cancellableTask);
TimeUnit.SECONDS.sleep(1); // Wait for some time
cancellableTask.cancel();
executorService.shutdown();
}
public static class CancellableTask implements Runnable {
private volatile boolean shouldStop;
@Override
public void run() {
while (!shouldStop) {
// Do work.
}
System.out.println("Stopping.");
}
public void cancel() {
shouldStop = true;
}
}
}
In this example, we're defining our task in the CancellableTask class. Note that unlike the previous example, every CancellableTask instance has its own cancellation flag - shouldStop. In the run method, it checks the flag and exits if it's set.
On the task submission side, we have the startAndCancel method. In this method, after initializing a thread pool, we're creating an instance of CancellableTask. As we mentioned, we must store a reference to the task - cancellableTask. Then we're canceling the task using its cancel method. As a result, the task exits when it sees that shouldStop is true.
4. Use Thread Interruption
Next, we'll look at the approaches that rely on thread interruption using Thread.interrupt.
4.1. Cancel with Thread.interrupt
Firstly, we'll cancel a task running on a dedicated Thread by invoking Thread.interrupt.
This approach is applicable only if the application code owns the thread. For example, if the application is using a thread pool, the ExecutorService implementation - not the application - owns the threads. Thus we mustn't directly interrupt the worker threads in a pool.
public class CancelTaskWithInterruption {
public void startAndCancel() throws InterruptedException {
final Runnable task = () -> {
while (!Thread.currentThread().isInterrupted()) {
// Do work.
}
System.out.println("Stopping.");
};
final Thread thread = new Thread(task);
thread.start();
TimeUnit.SECONDS.sleep(1); // Wait for some time
thread.interrupt();
}
}
In this example, we have one method, startAndCancel. In this method, we're first defining the Runnable task. It continues to run unless the thread is interrupted - checking Thread.currentThread().isInterrupted(). Then we're starting a Thread to run our task. The task and thread run until we interrupt the thread invoking Thread.interrupt. After this invocation, firstly the task completes and then the thread terminates.
4.2. Cancel with Future.cancel
The previous approach doesn't help us when we execute our tasks using an ExecutorService. Basically, we mustn't interrupt a worker thread. Instead, we must rely on the library classes to do this for us. The Future class offers the cancel method to cancel a task - possibly by thread interruption.
boolean cancel(boolean mayInterruptIfRunning);
Remember that when we submit a task to a thread pool, it may not start immediately. Instead, it can go to the task queue if all threads are busy. cancel removes the task if it's in the queue. Also, when we pass mayInterruptIfRunning as true, the method interrupts the task if it's running.
public class CancelTaskInPoolWithInterruption {
public void startAndCancel() throws InterruptedException {
final ExecutorService executorService = Executors.newSingleThreadExecutor();
final Runnable task = () -> {
while (!Thread.currentThread().isInterrupted()) {
// Do work.
}
System.out.println("Stopping.");
};
final Future<?> future = executorService.submit(task);
TimeUnit.SECONDS.sleep(1); // Wait for some time
future.cancel(true);
executorService.shutdown();
}
}
Here, we're creating a thread pool invoking Executors.newSingleThreadExecutor. When we submit our task to the pool, it returns a Future - a handle to the task. After waiting for a second, we're invoking cancel with true as the argument. As a result, the thread pool interrupts the worker thread executing the task. Since our task checks the thread interruption status, it stops the execution.
As a final note, if the task we want to cancel is already completed or canceled, cancel has no effect.
4.3. Cancel with ExecutorService.shutdownNow
Now, we'll look at the shutdownNow method of ExecutorService in terms of task cancellation. If there are running tasks at the invocation time, shutdownNow interrupts the worker threads executing them:
public class CancelTaskInPoolWithShutdownNow {
public static void main(String[] args) throws InterruptedException {
final CancelTaskInPoolWithShutdownNow cancellation = new CancelTaskInPoolWithShutdownNow();
cancellation.startAndCancel();
}
public void startAndCancel() throws InterruptedException {
final ExecutorService executorService = Executors.newSingleThreadExecutor();
final Runnable task = () -> {
while (!Thread.currentThread().isInterrupted()) {
// Do work.
}
System.out.println("Stopping.");
};
executorService.submit(task);
TimeUnit.SECONDS.sleep(1); // Wait for some time
executorService.shutdownNow();
}
}
In this example, newSingleThreadExecutor returns a thread pool wrapping a ThreadPoolExecutor instance. And ThreadPoolExecutor uses Thread.interrupt as its cancellation mechanism. As a result, when we invoke shutdownNow, it cancels our task interrupting its worker thread.
5. Summary
In this tutorial, we've looked at how we can cancel a task running on a Thread. Firstly, we examined the approaches using a cancellation flag. Then we covered the ones relying on the thread interruption status. We can conclude that task cancellation requires a cooperative mechanism between the task and the requester.
Lastly, check out the source code for all examples in this tutorial over on Github.