← Back to Multithreading Course

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.

One important point:

InterruptedException and ExecutionException represent different situations.

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.

  1. The ExecutorService starts the task.
  2. The task repeatedly prints a message.
  3. The task sleeps for one second between iterations.
  4. The main thread waits for three seconds.
  5. The main thread calls future.cancel(true).
  6. Java requests interruption of the worker thread.
  7. Thread.sleep() throws InterruptedException.
  8. 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.

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

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.

Congratulations! Part 5 Completed 🎉

We have completed Part 5 — Managing Threads with the Executor Framework.

Let's take a moment to appreciate what we have learned.

  • Why we need the Executor Framework.
  • Executor and ExecutorService.
  • Thread pools and fixed thread pools.
  • Properly shutting down an ExecutorService.
  • Runnable vs. Callable.
  • Getting task results using Future.
  • Scheduling tasks with ScheduledExecutorService.
  • Handling task failures and cancellation.

We now understand how Java helps us manage tasks without manually creating and controlling every thread.

Coming Up: Part 6 — Locks and Concurrent Collections

So far, we have learned about synchronized methods, synchronized blocks, and several concurrency utilities.

Java also provides more advanced tools for controlling access to shared data and working safely with collections when multiple threads are involved.

In Part 6, we will explore ReentrantLock, ReadWriteLock, thread-safe collections, ThreadLocal, and immutable objects.

Let's keep learning, one concept at a time! See you in Class 34.

0% Completed

You have completed 33 out of 46 classes.