Class 33: Handling Task Failures and Cancellation
Part 5 — Managing Threads with the Executor Framework
In the previous classes, we learned how to submit tasks using ExecutorService, retrieve results using Future, and schedule tasks using ScheduledExecutorService.
But real applications don't always work perfectly.
Sometimes a task fails because of an exception. Sometimes we no longer need a task and want to cancel it.
Let's understand how Java helps us handle these situations.
1. What Happens When a Callable Task Fails?
Remember that a Callable task can return a result and can also throw an exception.
Consider this example:
Callable<Integer> task = () -> {
int result = 10 / 0;
return result;
};
We are trying to divide a number by zero. This causes an ArithmeticException.
Now imagine that we submit this task to an ExecutorService.
Future<Integer> future = executorService.submit(task);
The task executes on a worker thread. If it fails, the exception is captured as part of the Future's outcome.
When we call future.get(), Java throws
an ExecutionException.
The original exception can be accessed using
getCause().
2. Handling Task Failures Using Future
Let's write a complete example.
import java.util.concurrent.*;
public class TaskFailureExample {
public static void main(String[] args) {
ExecutorService executorService =
Executors.newSingleThreadExecutor();
Callable<Integer> task = () -> {
System.out.println("Task started");
int result = 10 / 0;
return result;
};
Future<Integer> future =
executorService.submit(task);
try {
Integer result = future.get();
System.out.println("Result: " + result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
System.out.println("Task failed!");
System.out.println(
"Reason: " + e.getCause()
);
} finally {
executorService.shutdown();
}
}
}
Let's understand the important parts.
- The Callable task attempts to divide 10 by zero.
- The task throws an ArithmeticException.
- We call
future.get()to retrieve its result. - Because the task failed, get() throws ExecutionException.
- We use
getCause()to inspect the original exception.
One important point:
InterruptedException and
ExecutionException represent different situations.
- InterruptedException: The thread waiting in get() was interrupted.
- ExecutionException: The task itself failed while executing.
3. What If We Submit a Runnable Task?
So far, we have used Callable. But we can also submit Runnable tasks to an ExecutorService.
Future<?> future =
executorService.submit(() -> {
System.out.println("Task is running");
throw new RuntimeException("Something went wrong");
});
Even though Runnable does not return a result, submit() still returns a Future.
We can call future.get() to wait for the task
to finish and discover whether it failed.
If the Runnable task throws an exception, get() throws ExecutionException.
This is useful because Future allows us to observe the completion and failure of submitted tasks.
4. Why Do We Need Task Cancellation?
Let's consider another situation.
Imagine that our application starts a task to download a large file.
After a few seconds, the user changes their mind and no longer wants the download.
Should we continue running a task that is no longer required?
Not necessarily.
Java provides the cancel() method through
the Future interface.
future.cancel(true);
It attempts to cancel the task.
5. Understanding cancel()
The cancel() method accepts a boolean argument:
future.cancel(true);
future.cancel(false);
Let's understand the difference.
cancel(true)
Java may interrupt the worker thread if the task is already running.
This sends an interruption request to the thread.
The task should cooperate with interruption to stop its work properly.
cancel(false)
Java does not interrupt a task that has already started.
However, if the task has not started yet, cancellation can prevent it from starting.
Remember:
Cancellation does not forcibly kill a running thread. Java uses interruption as a way to request that the task stop.
6. Understanding Interruption During Cancellation
Let's look at a task that performs repeated work.
import java.util.concurrent.*;
public class TaskCancellationExample {
public static void main(String[] args)
throws InterruptedException {
ExecutorService executorService =
Executors.newSingleThreadExecutor();
Future<?> future = executorService.submit(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Task is working...");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Task received interruption");
Thread.currentThread().interrupt();
}
System.out.println("Task is stopping");
});
Thread.sleep(3000);
System.out.println("Requesting cancellation");
future.cancel(true);
executorService.shutdown();
}
}
Let's understand the execution.
- The ExecutorService starts the task.
- The task repeatedly prints a message.
- The task sleeps for one second between iterations.
- The main thread waits for three seconds.
- The main thread calls
future.cancel(true). - Java requests interruption of the worker thread.
- Thread.sleep() throws InterruptedException.
- The task handles the interruption and exits.
Notice that the task itself is responsible for responding to the interruption.
This is why we say cancellation is a request, not a guarantee that Java will forcibly stop the thread.
7. Checking Whether a Task Was Cancelled
Future provides two useful methods:
future.isCancelled();
future.isDone();
Let's understand them.
isCancelled()returns true if the task was successfully cancelled.isDone()returns true if the task has completed, failed, or been cancelled.
A cancelled task is also considered done.
However, a completed task is not necessarily cancelled.
8. Cancelling a Task That Has Not Started Yet
Imagine that we have a thread pool with only one worker.
We submit two tasks. The first task starts running, while the second task waits in the queue.
If we cancel the second task before it starts, Java can prevent it from executing.
import java.util.concurrent.*;
public class CancelWaitingTaskExample {
public static void main(String[] args)
throws InterruptedException {
ExecutorService executorService =
Executors.newSingleThreadExecutor();
Future<?> firstTask = executorService.submit(() -> {
try {
System.out.println("First task started");
Thread.sleep(5000);
System.out.println("First task completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Future<?> secondTask = executorService.submit(() -> {
System.out.println("Second task started");
});
boolean cancelled = secondTask.cancel(false);
System.out.println("Second task cancelled: " + cancelled);
executorService.shutdown();
}
}
Since the executor has only one worker, the first task occupies that worker.
The second task is waiting for its turn. We cancel it before it starts.
Therefore, the second task will not execute if cancellation succeeds.
9. A Few Important Things to Remember
- Future.get() can throw ExecutionException when a task fails.
- Use getCause() to inspect the original exception.
- Future.cancel() attempts to cancel a task.
- cancel(true) may interrupt a running task.
- cancel(false) does not interrupt a task that has already started.
- A running task should cooperate with interruption.
- isCancelled() checks whether cancellation succeeded.
- isDone() also returns true for cancelled or failed tasks.
These concepts help us build applications that can respond properly when tasks fail or are no longer needed.
Test Your Understanding
Let's see how well you understood this class!
Question 1: What exception does Future.get() throw when a Callable task fails?
Question 2: What does future.cancel(true) attempt to do?
Question 3: What does future.cancel(false) do to a task that is already running?
Question 4: Which method checks whether a Future was successfully cancelled?
Question 5: What should a running task do to respond properly to interruption?
Class Summary
Let's quickly revise what we learned today.
- Callable task failures are reported through ExecutionException when we call Future.get().
- getCause() helps us inspect the original exception.
- Future.cancel() attempts to cancel a task.
- cancel(true) may interrupt a running task.
- cancel(false) does not interrupt a running task.
- Cancellation does not forcibly kill a thread.
- isCancelled() checks whether cancellation succeeded.
- isDone() returns true for completed, failed, or cancelled tasks.
Remember: Java provides ways to request cancellation and report task failures, but our tasks should be designed to handle these situations properly.
You have completed 33 out of 46 classes.