9 errores que hay que evitar al programar en Java
¿Qué errores deben evitarse al programar en Java? En el siguiente artículo responderemos a esta pregunta.
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.
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. Concurrenc in Javay 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.
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.
Hilo
es un 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 Hilo
class. However, it is not a recommended approach. Threads
should be used as a mechanism that run the task. Tasks are pieces of código that we want to run in a concurrent mode. We can define them using the Runnable
interfaz.
But enough theory, let’s put our code where our mouth is.
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 inHilo
. 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.
When there are a lot of tasks, creating a separate thread for each one is not a good idea. Creating a Hilo
es un
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 therun()
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 createExecutorService
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();
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 Excepción
.
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 Futuro
?Futuro
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 Futuro
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.
The issue with Futuro
is that it works in the ‘push paradigm’. When using Futuro
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 Futuro
what to do when it is ready with its task. Unfortunately,Futuro
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 toComputableFuture
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()
que
takes Runnable
. A callback – a piece of code that should be run on task completion – is defined by thenAccept()
método.
Java provides a lot of different approaches to concurrency. In this article, we barely touched on the topic.
Nevertheless, we covered the basics of Hilo
, Runnable
, Callable
y CallableFuture
which poses a good point
for farther topic investigation.