← Back to Multithreading Course

Class 31: Getting Results with Future

Part 5 — Managing Threads with the Executor Framework

In the previous class, we learned about Runnable and Callable.

We understood that Runnable is used when a task does not need to return a result, whereas Callable can return a result.

Now let's consider a practical situation.

We submit a Callable task to an ExecutorService. The task runs on a worker thread and calculates something.

How do we get the result back?

Java provides an interface called Future for this purpose.

Let's understand it step by step.

1. What Is Future?

A Future represents the result of a task that may finish at some point in the future.

Think about ordering food online.

You place an order, and the restaurant starts preparing it. You do not receive the food immediately.

Instead, you receive an order reference that lets you check the status of your order and eventually receive the food.

A Future works in a similar way.

In simple words, Future is a reference to the result of an asynchronous task.

2. Submitting a Callable Task

Let's start with a simple example.

We will create a Callable task that calculates the sum of two numbers.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class FutureExample {

    public static void main(String[] args) throws Exception {

        ExecutorService executorService =
                Executors.newSingleThreadExecutor();

        Callable<Integer> task = () -> {

            int a = 10;
            int b = 20;

            return a + b;
        };

        Future<Integer> future =
                executorService.submit(task);

        Integer result = future.get();

        System.out.println("Result: " + result);

        executorService.shutdown();
    }
}

Let's understand the important lines.

Step 1: Create a Callable Task

Callable<Integer> task = () -> {

    int a = 10;
    int b = 20;

    return a + b;
};

This task calculates the sum of two numbers and returns an Integer value.

Step 2: Submit the Task

Future<Integer> future =
        executorService.submit(task);

We submit the Callable task using the submit() method.

Notice that submit() returns a Future object.

Because our Callable returns an Integer, we use Future<Integer>.

Step 3: Retrieve the Result

Integer result = future.get();

This is the most important line in our example.

The get() method retrieves the result of the task.

If the task has not finished yet, get() waits until the result becomes available.

Once the task completes, the result is returned.

In our example, the result will be:

Result: 30

3. Does future.get() Always Return Immediately?

No.

This is an important concept.

Imagine a task that takes five seconds to complete. If we call future.get() immediately after submitting it, our current thread may have to wait for those five seconds.

The get() method is a blocking operation.

In simple words, the thread calling get() waits until the result is ready.

4. Understanding Blocking with an Example

Let's modify our example slightly.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class FutureBlockingExample {

    public static void main(String[] args) throws Exception {

        ExecutorService executorService =
                Executors.newSingleThreadExecutor();

        Callable<Integer> task = () -> {

            System.out.println("Task started");

            Thread.sleep(3000);

            System.out.println("Task completed");

            return 100;
        };

        Future<Integer> future =
                executorService.submit(task);

        System.out.println("Waiting for result");

        Integer result = future.get();

        System.out.println("Result: " + result);

        executorService.shutdown();
    }
}

Let's understand the execution.

  1. We submit the task to the ExecutorService.
  2. The task starts executing on a worker thread.
  3. The worker thread sleeps for three seconds.
  4. The main thread calls future.get().
  5. The main thread waits until the task finishes.
  6. The result is returned, and the main thread continues.

One important thing to understand:

The worker thread and the main thread are different threads. The worker performs the task while the main thread waits for the result.

5. Checking Whether a Task Has Completed

What if we don't want to wait immediately?

What if we simply want to check whether the task has completed?

Future provides a method called isDone().

future.isDone();

It returns:

Let's see a simple example.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class FutureIsDoneExample {

    public static void main(String[] args) throws Exception {

        ExecutorService executorService =
                Executors.newSingleThreadExecutor();

        Future<String> future = executorService.submit(() -> {

            Thread.sleep(3000);

            return "Task completed";

        });

        System.out.println("Is task done? " + future.isDone());

        String result = future.get();

        System.out.println("Result: " + result);

        System.out.println("Is task done? " + future.isDone());

        executorService.shutdown();
    }
}

When we check isDone() immediately after submission, it may return false because the task is still running.

After get() returns successfully, the task has completed, so isDone() will return true.

Remember that the first result is not guaranteed to be false. A very quick task may already have completed before we check it.

6. Retrieving a Result with a Time Limit

We have seen that get() waits until the task completes.

But sometimes we don't want to wait indefinitely.

Future provides another version of get() that accepts a time limit.

future.get(5, TimeUnit.SECONDS);

This means:

Wait for the result for a maximum of five seconds.

If the task does not complete within that time, Java throws a TimeoutException.

Let's look at an example.

import java.util.concurrent.*;

public class FutureTimeoutExample {

    public static void main(String[] args) {

        ExecutorService executorService =
                Executors.newSingleThreadExecutor();

        Future<String> future = executorService.submit(() -> {

            Thread.sleep(5000);

            return "Task completed";

        });

        try {

            String result = future.get(2, TimeUnit.SECONDS);

            System.out.println(result);

        } catch (TimeoutException e) {

            System.out.println("Task took too long!");

        } catch (InterruptedException e) {

            Thread.currentThread().interrupt();

        } catch (ExecutionException e) {

            System.out.println("Task failed: " + e.getCause());

        } finally {

            executorService.shutdown();
        }
    }
}

Here, the task takes five seconds, but we wait for only two seconds.

Therefore, the get() call times out and throws TimeoutException.

Notice that a timeout does not automatically cancel the task. The task may continue running in the background.

7. Cancelling a Task

Sometimes we may decide that a task is no longer required.

Future provides a method called cancel().

future.cancel(true);

It attempts to cancel the task.

The method accepts a boolean argument:

Cancellation is not a guarantee that running code will immediately stop. The task should cooperate with interruption.

We will explore cancellation and task failures in more detail in Class 33.

8. Important Future Methods

Let's quickly revise the methods we have learned.

Method Purpose
get() Waits for the result and retrieves it.
get(time, unit) Waits for the result up to a specified time.
isDone() Checks whether the task has completed, failed, or been cancelled.
cancel(mayInterrupt) Attempts to cancel the task.
isCancelled() Checks whether the task was successfully cancelled.

9. What Happens If a Callable Task Fails?

So far, we have seen tasks that complete successfully.

But what if a task throws an exception?

When a Callable task fails, the exception is captured by the Future.

When we call get(), Java throws an ExecutionException.

The original exception is available through getCause().

Let's look at a small example.

import java.util.concurrent.*;

public class FutureExceptionExample {

    public static void main(String[] args) {

        ExecutorService executorService =
                Executors.newSingleThreadExecutor();

        Future<Integer> future = executorService.submit(() -> {

            throw new IllegalArgumentException("Invalid input");

        });

        try {

            Integer result = future.get();

        } catch (InterruptedException e) {

            Thread.currentThread().interrupt();

        } catch (ExecutionException e) {

            System.out.println(
                "Task failed: " + e.getCause()
            );

        } finally {

            executorService.shutdown();
        }
    }
}

The task throws IllegalArgumentException. However, when we retrieve the result using get(), we receive an ExecutionException.

We can inspect the original exception using e.getCause().

We will study task failure handling in greater detail in a later class.

10. What We Learned Today

Future gives us a way to work with the result of a task that executes asynchronously.

We can retrieve its result, check its completion status, wait for a limited time, or attempt to cancel it.

The most important concept to remember is that future.get() blocks the calling thread until the result is available.

In the next class, we will learn how to execute tasks at a particular time or repeatedly using ScheduledExecutorService.

Test Your Understanding

Let's see how well you understood this class!

Question 1: What does a Future represent?

Question 2: Which method retrieves the result of a Future?

Question 3: What happens when we call future.get() before the task finishes?

Question 4: What does isDone() check?

Question 5: What happens if get() exceeds its specified time limit?

Class Summary

Let's quickly revise what we learned today.

  • Future represents the result of an asynchronous task.
  • ExecutorService.submit() returns a Future object.
  • get() retrieves the result and blocks until it is available.
  • isDone() checks whether the task has reached a completed state.
  • Timed get() allows us to wait for a limited time.
  • cancel() attempts to cancel a task.
  • ExecutionException is used to report a failure when retrieving a failed task's result.

Remember: Future gives us a way to check on a submitted task and retrieve its result when it becomes available.

Next Class: Running Scheduled Tasks with ScheduledExecutorService

So far, we have submitted tasks for execution as soon as worker threads become available.

But what if we want a task to run after five seconds? Or what if we want it to run repeatedly at regular intervals?

In the next class, we will learn about ScheduledExecutorService and how to schedule tasks in Java.

Let's keep learning, one concept at a time!

0% Completed

You have completed 31 out of 46 classes.