Threads and Multithreading – Part 7

Threads - images of multicoloured threads

This is part 7 of this series. If you missed any of the last six, you can find them here:

Introduction

In the last post we revised our knowledge of the Runnable interface, and then we introduced the Callable interface. In this post we’ll look at how to create Executor and ExecutorService objects. We’ll also and how to run Callable objects and access their return values using the Future interface.

Executors and Executor Services

We know that we can pass a Runnable object to a Thread constructor, and then start the thread object. This means that we must manually control the lifecycle of a thread. It is error-prone and code-intensive.

The Concurrency API provides much easier ways to run threads. One of the main ways is by using concrete class implementations of the Executor and ExecutorService interfaces.

Let’s start with the Executor interface.

public interface Executor {
    public void execute(Runnable runnable);
}

The Executor interface executes Runnable tasks. Instead of explicitly creating threads, all we do is submit a Runnable task to an Executor implementation, and it does the rest. We don’t have to worry about the actual mechanics of how each task is run.

For example, usually we would create the following code for each task we want to run:

Thread thread1 = new Thread(new RunnableTask1());
thread1.start();
Thread thread2 = new Thread(new RunnableTask2());
thread2.start();

Using an Executor we will just write the following code:

Executor executor = aConcreteExecutor;  // created by a factory; see later
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());

The ExecutorService interface extends the functionality of the Executor interface by adding a number of extra methods. Most of these methods are focused on running Callable tasks, including:

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit);

<T> T invokeAny(Collection<? extends Callable<T>> tasks);
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit);

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

We can see that even though Runnable tasks don’t return values, we can submit a Runnable with an extra parameter of the desired result type and value.

We’ll use the submit() methods after we learn how to create ExecutorService objects.

Creating Executors and ExecutorServices

We can create Executor and ExecutorService objects by using the factory methods of the Executors class. These static methods include overloaded versions of the following methods (all returning an ExecutorService object):

  • newCachedThreadPool()
  • newFixedThreadPool()
  • newScheduledThreadPool()
  • newSingleThreadExecutor()
  • newSingleThreadScheduledExecutor()
  • newWorkStealingPool()

All of these factory methods can be called in a similar way to the following:

ExecutorService executorService = ExecutorService.newCachedThreadPool();

Then we can use the executorService object to invoke collections of Callable tasks and/or submit either Callable or Runnable tasks to it.

We can also create our own ExecutorService instances by using the ForkJoinPool, ThreadPoolExecutor, and ScheduledThreadPoolExecutor classes if we want more control over the creation process.

Submitting Callable Objects

Both Callable and Runnable objects can be submitted to executor services. Runnable objects don’t return values, while Callable objects do. However, we’ve already seen that we can submit a Runnable task with a default return value. How do we access the returned values from these tasks?

Since the submit() method executes the task asynchronously, it doesn’t block while we wait for the task to complete. The executor service can’t return the result of the Callable directly. Instead the service returns a result of type Future which can be used to retrieve the actual result at a later stage.

Let’s use the Callable task from last week’s post as an example:

Callable<Integer> task = () -> {
    try {
        Thread.sleep(1000); // 1000 milliseconds
        return 42;
    }
    catch (InterruptedException e) {
        throw new IllegalStateException("Task interrupted!", e);
    }
};

Now we can create an ExecutorService object to run the Callable task:

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);

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

Integer result = future.get(); // blocks here until the Future is done

System.out.println("Future done? " + future.isDone());
System.out.println("Result of future = " + result);

In this code we submit the Callable task. If we immediately check whether the Future has finished,isDone() will return false because there’s a one second delay in the task. Calling the method get() blocks the current thread and waits until the task has finished before returning the result. At this stage, calling isDone() now returns true.

The output will be the following:

Future done? false
Future done? true
Result = 42

Don’t worry if you don’t entirely understand the previous code. In the next post, we’ll cover Futures in more detail. For the time being, think of a Future as a placeholder variable that stands in for a future result of a computation. We can submit a task to an executor service, and pretty much forget about it until we need the result of the task. At that point all we have to do is to query the Future with future.isDone() to find out whether the task is done. If so, we can get the result by calling future.get(). If not, we can either get busy with something else, or we can block at that point to wait for the task to complete.

Another thing we’ll look at next week is shutting down the executor service correctly. If you type in the previous code and run it, you’ll find that the program hangs and doesn’t terminate. You have to press Ctrl-C on Windows/Linux to terminate the JVM.

Has this series helped you? Do you have more questions about threads? Please share your thoughts and comments.

Until then, stay safe and keep learning!

Leave a Comment

Your email address will not be published. Required fields are marked *

Code like a Java Guru!

Thank You

We're Excited!

Thank you for completing the form. We're excited that you have chosen to contact us about training. We will process the information as soon as we can, and we will do our best to contact you within 1 working day. (Please note that our offices are closed over weekends and public holidays.)

Don't Worry

Our privacy policy ensures your data is safe: Incus Data does not sell or otherwise distribute email addresses. We will not divulge your personal information to anyone unless specifically authorised by you.

If you need any further information, please contact us on tel: (27) 12-666-2020 or email info@incusdata.com

How can we help you?

Let us contact you about your training requirements. Just fill in a few details, and we’ll get right back to you.

Your Java tip is on its way!

Check that incusdata.com is an approved sender, so that your Java tips don’t land up in the spam folder.

Our privacy policy means your data is safe. You can unsubscribe from these tips at any time.