Concurrency in Java Part 1 - Introduction
Software Development
Rafal Sawicki
Rafal Sawicki
Java Developer
2022-06-15

Concurrency in Java Part 1 - Introduction

Read the first part of our blog series devoted to concurrency in Java. In the following article we will take a closer look at differences between thread and processes, thread pools, executors and many more!

In general, the conventional programming approach is sequential. Everything in a program happens one step at a time. But, in fact, the parallel is how the whole world runs – it is the ability to execute more than one task simultaneously.

Thread vs. process

To discuss such advanced topics as concurrency in Java or multithreading, we have to settle on some common definitions to be sure we are on the same page.

Let's start with the basics. In the non-sequential world, we have two kinds of concurrency representants: processes and threads. A process is an instance of the program running. Normally, it is isolated from other processes. The operating system is responsible for assigning resources to each process. Moreover, it acts as a conductor that schedules and controls them.

Thread is a kind of a process but on a lower level, therefore it is also known as light thread. Multiple threads can run in one process. Here the program acts as a scheduler and a controller for threads. This way individual programs appear to do multiple tasks at the same time.

The fundamental difference between threads and processes is the isolation level. The process has its own set of resources, whereas the thread shares data with other threads. It may seem like an error-prone approach and indeed it is. For now, let's not focus on that as it's beyond the scope of this article.

Processes, threads - ok... But what exactly is concurrency? Concurrency means you can execute multiple tasks at the same time. It does not mean those tasks have to run simultaneously – that's what parallelism is. Concurrency in Java also does not require you to have multiple CPUs or even multiple cores. It can be achieved in a single-core environment by leveraging context switching.

A term related to concurrency is multithreading. This is a feature of programs that allows them to execute several tasks at once. Not every program uses this approach but the ones that do can be called multithreaded.

We are almost ready to go, just one more definition. Asynchrony means that a program performs non-blocking operations. It initiates a task and then goes on with other things while waiting for the response. When it gets the response, it can react to it.

All that jazz

By default, each Java application runs in one process. In that process, there is one thread related to the main() method of an application. However, as mentioned, it is possible to leverage the mechanisms of multiple threads within one program.

Runnable

Thread is a Java class in which the magic happens. This is the object representation of the before mentioned thread. To create your own thread, you can extend the Thread class. However, it is not a recommended approach. Threads should be used as a mechanism that run the task. Tasks are pieces of code that we want to run in a concurrent mode. We can define them using the Runnable interface.

But enough theory, let's put our code where our mouth is.

Problem

Assume we have a couple of arrays of numbers. For each array, we want to know the sum of the numbers in an array. Let's pretend there are a lot of such arrays and each of them is relatively big. In such conditions, we want to make use of concurrency and sum each array as a separate task.

int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};

Runnable task1 = () -> {
    int sum = Arrays.stream(a1).sum();
    System.out.println("1. The sum is: " + sum);
};

Runnable task2 = () -> {
    int sum = Arrays.stream(a2).sum();
    System.out.println("2. The sum is: " + sum);
};

Runnable task3 = () -> {
    int sum = Arrays.stream(a3).sum();
    System.out.println("3. The sum is: " + sum);
};

new Thread(task1).start();
new Thread(task2).start();
new Thread(task3).start();

As you can see from the code above Runnable is a functional interface. It contains a single abstract method run() with no arguments. The Runnable interface should be implemented by any class whose instances are intended to be executed by a thread.

Once you have defined a task you can create a thread to run it. This can be achieved via new Thread() constructor that takes Runnable as its argument.

The final step is to start() a newly created thread. In the API there are also run() methods in Runnable and in Thread. However, that is not a way to leverage concurrency in Java. A direct call to each of these methods results in executing the task in the same thread the main() method runs.

Thread pools and Executors

When there are a lot of tasks, creating a separate thread for each one is not a good idea. Creating a Thread is a heavyweight operation and it's far better to reuse existing threads than to create new ones.

When a program creates many short-lived threads it is better to use a thread pool. The thread pool contains a number of ready-to-run but currently not active threads. Giving a Runnable to the pool causes one of the threads calls the run() method of given Runnable. After completing a task the thread still exists and is in an idle state.

Ok, you get it - prefer thread pool instead of manual creation. But how can you make use of thread pools? The Executors class has a number of static factory methods for constructing thread pools. For example newCachedThredPool() creates a pool in which new threads are created as needed and idle threads are kept for 60 seconds. In contrast, newFixedThreadPool() contains a fixed set of threads, in which idle threads are kept indefinitely.

Let's see how it could work in our example. Now we do not need to create threads manually. Instead, we have to create ExecutorService which provides a pool of threads. Then we can assign tasks to it. The last step is to close the thread pool to avoid memory leaks. The rest of the previous code stays the same.

ExecutorService executor = Executors.newCachedThreadPool();

executor.submit(task1);
executor.submit(task2);
executor.submit(task3);

executor.shutdown();

Callable

Runnable seems like a nifty way of creating concurrent tasks but it has one major shortcoming. It cannot return any value. What is more, we cannot determine if a task is finished or not. We also do not know if it has been completed normally or exceptionally. The solution to those ills is Callable.

Callable is similar to Runnable in a way it also wraps asynchronous tasks. The main difference is it is able to return a value. The return value can be of any (non-primitive) type as the Callable interface is a parameterized type. Callable is a functional interface that has call() method which can throw an Exception.

Now let's see how we can leverage Callable in our array problem.

int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};

Callable<Integer> task1 = () -> Arrays.stream(a1).sum();
Callable<Integer> task2 = () -> Arrays.stream(a2).sum();
Callable<Integer> task3 = () -> Arrays.stream(a3).sum();

ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> future1 = executor.submit(task1);
Future<Integer> future2 = executor.submit(task2);
Future<Integer> future3 = executor.submit(task3);

System.out.println("1. The sum is: " + future1.get());
System.out.println("2. The sum is: " + future2.get());
System.out.println("3. The sum is: " + future3.get());

executor.shutdown();

Okay, we can see how Callable is created and then submitted to ExecutorService. But what the heck is Future? Future acts as a bridge between threads. The sum of each array is produced in a separate thread and we need a way to get those results back to main().

To retrieve the result from Future we need to call get() method. Here can one of two things happen. First, the result of the computation performed by Callable is available. Then we get it immediately. Second, the result is not ready yet. In that case get() method will block until the result is available.

ComputableFuture

The issue with Future is that it works in the 'push paradigm'. When using Future you have to be like a boss who constantly asks: 'Is your task done? Is it ready?' until it provides a result. Acting under constant pressure is expensive. Way better approach would be to order Future what to do when it is ready with its task. Unfortunately, Future cannot do that but ComputableFuture can.

ComputableFuture works in 'pull paradigm'. We can tell it what to do with the result when it completed its tasks. It is an example of an asynchronous approach.

ComputableFuture works perfectly with Runnable but not with Callable. Instead, it is possible to supply a task to ComputableFuture in form of Supplier.

Let's see how the above relates to our problem.

int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};

CompletableFuture.supplyAsync(() -> Arrays.stream(a1).sum())
                .thenAccept(System.out::println);

CompletableFuture.supplyAsync(() -> Arrays.stream(a2).sum())
                .thenAccept(System.out::println);

CompletableFuture.supplyAsync(() -> Arrays.stream(a3).sum())
                .thenAccept(System.out::println);

The first thing that strikes you is how much shorter this solution is. Besides that, it also looks neat and tidy.

Task to CompletableFuture can be provided by supplyAsync() method that takes Supplier or by runAsync() that takes Runnable. A callback - a piece of code that should be run on task completion - is defined by thenAccept() method.

Conclusions

Java provides a lot of different approaches to concurrency. In this article, we barely touched on the topic. For a detailed description of how threads work, see Chapter 17 of the Java specification here.

Nevertheless, we covered the basics of Thread, Runnable, Callable and CallableFuture which poses a good point for the farther topic investigation.

Meet Java expert

Read more:

The Right Way to Find Top Java Developers

The Best Type of Projects for Java

Top Programming Languages for Fintech Companies