Java Concurrency: The Basics
Java Concurrency: The Basics
The ExecutorService interface is the easiest and recommended way to create and manage async tasks. We must first obtain an instance of an ExecutorService by ...
Join the DZone community and get the full member experience.Join For Free
The Power of Multi-Threading in JavaThis article describes the basics of Java Concurrency API, and how to assign work to be done asynchronously.
A thread is the smallest unit of execution that can be scheduled by the operating system, while a process is a group of associated threads that execute in the same, shared environment.
You may also like: [DZone Refcard] Core Java Concurrency
We call “system thread” the thread created by the JVM that runs in the background of the application (e.g. garbage-collector), and a “user-defined thread” those which are created by the developer.
First, we need to know that in a standard Java program, all the work we implement is done in the main thread, so the work is done sequentially.
To change that behavior, we can create our own threads and program them to do the work “at the same time”, creating a multi-threaded environment.
We can create threads extending the Thread class or implement the Runnable interface. Both use the method
run() to execute asynchronous work. A thread must be started by its
start() method; otherwise, the work is not forked in a different thread.
Extending Thread Class
By extending the Thread class, we can override the
Given the two classes “Ping” and “Pong” below, they simulate three-time processing job that takes from 0 to 4 seconds to accomplish each one:
If we instantiate them and call each method
run(), the work will be done sequentially in the main thread, which is not our intention here:
The output will be:
But if we start the threads calling its start method, the work will be done asynchronously and there is no guarantee of the execution order.
The output may be:
Implementing Runnable Interface
Another way to create a thread is to implement the Runnable interface, which can be an advantage if your worker class already extends another class. Since there is no multiple-inheritance in Java, it would be impossible to extend to the Thread class.
The only modifications in the previous code is the implements keyword in worker classes:
And the instantiation of the thread is passing by the Runnable implementation in its constructor.
Again, there is no guarantee of the execution order.
Since the Runnable interface is a functional interface, we can use a lambda expression to create the Runnable instance. The readability of the code gets a little messy (in my opinion), but still, it is a way to go.
Threads Attributes and Methods
To ensure that the main thread finishes its execution after the worker threads, we can join it, pausing its execution while the worker threads are alive.
The output may be as follows, and it's guaranteed that the last line will be the “End of main thread” in the example above.
Threads have an attribute,
name, that we can use to identify them during program execution. The default name is
Thread-n, where ’n’ is the incremental number of creation.
Thread priority is a numeric (integer) value associated with a thread that is taken into consideration by the scheduler when determining which thread should currently be executing. There are static constants that help us out:
- Thread.MIN_PRIORITY // int value = 1
- Thread.NORM_PRIORITY // int value = 5, it's the default value
- Thread.MAX_PRIORITY // int value =10
An issue to be aware of is the synchronization between data accessed by threads. If a value is accessed by many threads to make computation, its results can vary depending on the concurrent read made.
For example, let's assume a simple counter class:
And the main class that consumes it using two async threads:
The output varies on a number equals or less than 1000. The issue is on the ‘cont’ attribute that can be incremented by the other thread while it's being read by the first one. To ensure that just one thread will be executing the
increment() method, we can use the synchronized keyword, so we always get the correct sum of values (‘1000’, in this case).
Interface ExecutorService and Threads/Callables
ExecutorService interface is the easiest and recommended way to create and manage async tasks. We must first obtain an instance of an
ExecutorService by using some factory methods and then send the service tasks to be processed.
To instantiate an
ExecutorService, we should use one of the
Executors static methods, depending on the way we want to schedule the tasks and the number of threads used to do the work.
Executors.newSingleThreadExecutor(): Creates a single-threaded executor. Results are processed sequentially.
Executors.newFixedThreadPool(int nrOfThreads): Creates a thread pool that reuses a fixed number of threads.
Executors.newCachedThreadPool(): Creates a thread pool that creates new
threads as needed, but will reuse previous threads when they are available.
Executors.newSingleThreadScheduledExecutor(): Creates a single-threaded executor that can schedule commands to run after a given delay or to execute periodically.
Executors.newScheduledThreadPool(int nrOfThreads): Creates a thread pool that can schedule commands to run after a given delay or to execute periodically.
First, we create a Runnable implementation:
And then, we can assign the work to an
ExecutorService to manage:
The output may be (no order guaranteed):
When we need to get a returned value, we should implement a
Callable instead of a
Callable interface has the
call() method that returns a value and can throw a checked exception.
When invoking a
Callable task within
ExecutorService, we get a
Future object, which represents the result of async computation when it is over:
We can use the
invokeAll() method to send a list of callables to be executed asynchronously:
Example of output:
Published at DZone with permission of Tiago Albuquerque . See the original article here.
Opinions expressed by DZone contributors are their own.