CodeGym /Courses /JAVA 25 SELF /ExecutorService, Callable, Future: running tasks

ExecutorService, Callable, Future: running tasks

JAVA 25 SELF
Level 54 , Lesson 1
Available

1. ExecutorService: manage threads like a pro

Why you shouldn’t just create threads via new Thread

At first, multithreading looks simple:

Thread t = new Thread(() -> {
    // do something
});
t.start();

This approach works, but quickly becomes a burden when there are many tasks. Each new Thread() call creates a new thread, and dozens or hundreds of threads start overloading the system. It’s also inconvenient to manage them: you need to track when they finish, what to do on errors, how to stop and reuse them.

This is where ExecutorService comes in—a smart thread dispatcher. You just hand it tasks, and it decides which thread and when to run them. As a result, everything works faster, more reliably, and without the headache.

How ExecutorService works

ExecutorService works on a simple but effective principle.

  • Inside it there is a thread pool—a pre-created set of worker threads (fixed or dynamic).
  • Tasks go into a queue and are picked up by free threads.
  • The service manages the lifecycle: you can wait for completion, shut the pool down gracefully, and free resources.

Creating an ExecutorService

The most common way is to use the factory methods from the Executors class:

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

ExecutorService executor = Executors.newFixedThreadPool(4); // 4 threads
  • newFixedThreadPool(N) — a pool of N threads (good for most workloads).
  • newCachedThreadPool() — a dynamic pool that creates threads as needed (careful: under a task avalanche you can hit memory limits).
  • newSingleThreadExecutor() — a single thread (sequential execution).

Example: running Runnable via ExecutorService

executor.submit(() -> {
    System.out.println("Hello from the thread pool!");
});

After you finish working with ExecutorService, you must shut it down properly:

executor.shutdown(); // Forbids adding new tasks, waits for the current ones to finish

Important: If you don’t call shutdown(), the program may not exit—the pool threads will wait for new tasks.

2. Runnable vs. Callable: different kinds of tasks

Before Java 5, if you wanted to run something in a thread, you wrote an implementation of Runnable. That’s a task that returns nothing and doesn’t throw checked exceptions.

Runnable task = () -> {
    System.out.println("Just working, returning nothing!");
};
executor.submit(task);

Callable: a task with a result (and exceptions)

Sometimes you want a task not just to “do something,” but to return a result—for example, a sum of numbers, a computation result, or data from a server. For that, the Callable<T> interface exists.

import java.util.concurrent.Callable;

Callable<Integer> sumTask = () -> {
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
};
  • The call() method returns a result of type T.
  • The call() method can throw a checked exception.

Analogy: Runnable — “go wash the dishes” (the result doesn’t matter), Callable — “go bring tea and tell me what temperature it is” (the result matters).

Running a Callable: to get a result, use executor.submit(...). It returns a Future<T>.

3. Future: a promise of a result

Future is a “promise” to return a result in the future. When you submit a task to ExecutorService, you get a Future from which you can later obtain the result, check whether the task has finished, or cancel it.

Core Future methods

  • T get() — get the result (waits until the task completes).
  • boolean isDone() — whether the task has completed.
  • boolean cancel(boolean mayInterruptIfRunning) — attempt to cancel the task.
  • boolean isCancelled() — whether the task was cancelled.

Example: running a Callable and getting the result

import java.util.concurrent.*;

public class ParallelSumApp {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Callable<Integer> sumTask = () -> {
            int sum = 0;
            for (int i = 1; i <= 100; i++) sum += i;
            return sum;
        };

        Future<Integer> future = executor.submit(sumTask);

        System.out.println("Task started; you can do something else...");

        // Get the result (the method blocks the thread if the task hasn't finished yet)
        Integer result = future.get();
        System.out.println("Computation result: " + result);

        executor.shutdown();
    }
}
  • The task is submitted to the thread pool.
  • While the task runs, the main thread can do something else.
  • When you need the result, call future.get() — the thread will wait if the task is still running.
  • As soon as the task finishes, the result is returned.

4. Practice: multiple tasks, waiting for completion

Often you need to launch several tasks at once and wait for all of them to finish. For example, you process an array of data, split it into parts, and compute the sum of each part in a separate task.

Example: summing array elements in chunks

import java.util.*;
import java.util.concurrent.*;

public class ParallelArraySum {
    public static void main(String[] args) throws Exception {
        int[] array = new int[1000];
        Arrays.setAll(array, i -> i + 1); // Fill with numbers from 1 to 1000

        ExecutorService executor = Executors.newFixedThreadPool(4);

        int chunkSize = array.length / 4;
        List<Future<Integer>> futures = new ArrayList<>();

        for (int i = 0; i < 4; i++) {
            int from = i * chunkSize;
            int to = (i == 3) ? array.length : (i + 1) * chunkSize;

            Callable<Integer> sumTask = () -> {
                int sum = 0;
                for (int j = from; j < to; j++) sum += array[j];
                System.out.println("Sum from " + from + " to " + (to - 1) + " = " + sum);
                return sum;
            };

            futures.add(executor.submit(sumTask));
        }

        int totalSum = 0;
        for (Future<Integer> f : futures) {
            totalSum += f.get(); // Wait for each task in turn
        }

        System.out.println("Total sum: " + totalSum);

        executor.shutdown();
    }
}

Here the array is split into 4 parts. For each part a task (Callable) is created that computes the sum. All tasks are submitted to the ExecutorService, returning Futures. In the end, we gather the results of all tasks and add them up.

In real-world scenarios it’s convenient to use invokeAll to wait for all tasks at once.

5. Handling errors when working with Future

When you call future.get(), if the task finished with an exception, it will be thrown as an ExecutionException. This is important: if something went wrong inside the task, you’ll learn about it only when calling get().

Example: exception handling

Callable<Integer> errorTask = () -> {
    throw new IllegalArgumentException("Something went wrong!");
};

Future<Integer> badFuture = executor.submit(errorTask);

try {
    badFuture.get();
} catch (ExecutionException e) {
    System.out.println("Task finished with an error: " + e.getCause());
}
  • An exception is thrown inside the task.
  • When calling get(), it is wrapped in an ExecutionException.
  • You can get the real cause via getCause().

6. Useful details

How to cancel a task

Future<?> f = executor.submit(() -> {
    while (true) {
        // Endless work
        if (Thread.currentThread().isInterrupted()) {
            System.out.println("I've been asked to stop!");
            break;
        }
    }
});

Thread.sleep(100); // Wait a tiny bit
f.cancel(true); // Try to cancel the task
  • cancel(true) tries to interrupt the task if it hasn’t finished yet.
  • Inside the task, it’s advisable to check Thread.currentThread().isInterrupted() and exit gracefully.

shutdown vs shutdownNow

shutdown() — a soft shutdown: forbids adding new tasks and lets current ones finish. Used most often.

shutdownNow() — a hard shutdown: attempts to interrupt active threads and returns the list of tasks that didn’t get to start. Use with care.

invokeAll and invokeAny

invokeAll(Collection<Callable<T>> tasks) runs all submitted tasks and waits until they all finish. Returns a list of Future.

invokeAny(Collection<Callable<T>> tasks) waits only for the first successfully completed task, returns its result, and cancels the others. Handy when the first successful answer is what matters.

7. Common mistakes when working with ExecutorService, Callable, and Future

Mistake #1: Not shutting down ExecutorService. If you forget to call shutdown(), the program may hang after main finishes because pool threads are waiting for new tasks.

Mistake #2: Waiting for the result right after submitting the task. If right after submit() you call get(), you won’t get the benefits of asynchrony—the thread will wait anyway. Do useful work in parallel and request the result when you actually need it.

Mistake #3: Ignoring exceptions in tasks. If you don’t handle ExecutionException when calling get(), you can miss important errors that occurred in the task.

Mistake #4: Using shared mutable variables without synchronization. If several tasks work with the same data, you need synchronization or thread-safe collections.

Mistake #5: Creating too many threads. Don’t make a pool with a number of threads that greatly exceeds the number of CPU cores—it can even slow execution down.

Mistake #6: Forgetting to cancel tasks. If a task is no longer needed, cancel it via cancel() to avoid wasting resources.

1
Task
JAVA 25 SELF, level 54, lesson 1
Locked
Sending an urgent package by drone 📦
Sending an urgent package by drone 📦
1
Task
JAVA 25 SELF, level 54, lesson 1
Locked
Decoding the Ancient Prophecy 🔮
Decoding the Ancient Prophecy 🔮
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION